SkillAgentSearch skills...

Quickbeam

JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM, and a built-in TypeScript toolchain.

Install / Use

/learn @elixir-volt/Quickbeam
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

QuickBEAM

JavaScript runtime for the BEAM — Web APIs backed by OTP, native DOM, and a built-in TypeScript toolchain.

JS runtimes are GenServers. They live in supervision trees, send and receive messages, and call into Erlang/OTP libraries — all without leaving the BEAM.

Installation

def deps do
  [{:quickbeam, "~> 0.7.1"}]
end

Requires Zig 0.15+ (installed automatically by Zigler, or use system Zig).

Quick start

{:ok, rt} = QuickBEAM.start()
{:ok, 3} = QuickBEAM.eval(rt, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.eval(rt, "'hello'.toUpperCase()")

# State persists across calls
QuickBEAM.eval(rt, "function greet(name) { return 'hi ' + name }")
{:ok, "hi world"} = QuickBEAM.call(rt, "greet", ["world"])

QuickBEAM.stop(rt)

BEAM integration

JS can call Elixir functions and access OTP libraries:

{:ok, rt} = QuickBEAM.start(handlers: %{
  "db.query" => fn [sql] -> MyRepo.query!(sql).rows end,
  "cache.get" => fn [key] -> Cachex.get!(:app, key) end,
})

{:ok, rows} = QuickBEAM.eval(rt, """
  const rows = await Beam.call("db.query", "SELECT * FROM users LIMIT 5");
  rows.map(r => r.name);
""")

JS can also send messages to any BEAM process:

// Get the runtime's own PID
const self = Beam.self();

// Send to any PID
Beam.send(somePid, {type: "update", data: result});

// Receive BEAM messages
Beam.onMessage((msg) => {
  console.log("got:", msg);
});

// Monitor BEAM processes
const ref = Beam.monitor(pid, (reason) => {
  console.log("process died:", reason);
});
Beam.demonitor(ref);

Beam API reference

| Category | API | Description | |---|---|---| | Bridge | Beam.call(name, ...args) | Call an Elixir handler (async) | | | Beam.callSync(name, ...args) | Call an Elixir handler (sync) | | | Beam.send(pid, message) | Send a message to a BEAM process | | | Beam.onMessage(callback) | Receive BEAM messages | | Process | Beam.self() | PID of the owning GenServer | | | Beam.spawn(script) | Spawn a new JS runtime as a BEAM process | | | Beam.register(name) | Register the runtime under a name | | | Beam.whereis(name) | Look up a registered runtime | | | Beam.monitor(pid, callback) | Monitor a process for exit | | | Beam.demonitor(ref) | Cancel a monitor | | | Beam.link(pid) / Beam.unlink(pid) | Bidirectional crash propagation | | Distribution | Beam.nodes() | List connected BEAM nodes | | | Beam.rpc(node, runtime, fn, ...args) | Remote call to another node | | Utilities | Beam.sleep(ms) / Beam.sleepSync(ms) | Async/sync sleep | | | Beam.hash(data, range?) | Non-cryptographic hash (:erlang.phash2) | | | Beam.escapeHTML(str) | Escape & < > " ' | | | Beam.which(bin) | Find executable on PATH | | | Beam.peek(promise) / Beam.peek.status(promise) | Read promise result without await | | | Beam.randomUUIDv7() | Monotonic sortable UUID | | | Beam.deepEquals(a, b) | Deep structural equality | | | Beam.nanoseconds() | Monotonic high-res timer | | | Beam.uniqueInteger() | Monotonically increasing unique integer | | | Beam.makeRef() | Create a unique BEAM reference | | | Beam.inspect(value) | Pretty-print any value (including PIDs/refs) | | Semver | Beam.semver.satisfies(version, range) | Check version against Elixir requirement | | | Beam.semver.order(a, b) | Compare two semver strings | | Password | Beam.password.hash(password, opts?) | PBKDF2-SHA256 hash | | | Beam.password.verify(password, hash) | Constant-time verification | | Introspection | Beam.version | QuickBEAM version string | | | Beam.systemInfo() | Schedulers, memory, atoms, OTP release | | | Beam.processInfo() | Memory, reductions, message queue |

Supervision

Runtimes and context pools are OTP children with crash recovery:

children = [
  {QuickBEAM,
   name: :renderer,
   id: :renderer,
   script: "priv/js/app.js",
   handlers: %{
     "db.query" => fn [sql, params] -> Repo.query!(sql, params).rows end,
   }},
  {QuickBEAM, name: :worker, id: :worker},

  # Context pool for high-concurrency use cases
  {QuickBEAM.ContextPool, name: MyApp.JSPool, size: 4},
]

Supervisor.start_link(children, strategy: :one_for_one)

{:ok, html} = QuickBEAM.call(:renderer, "render", [%{page: "home"}])

The :script option loads a JS file at startup. If the runtime crashes, the supervisor restarts it with a fresh context and re-evaluates the script.

Individual Context processes are typically started dynamically (e.g. from a LiveView mount) and linked to the connection process.

Context Pool

For high-concurrency scenarios (thousands of connections), use ContextPool instead of individual runtimes. Many lightweight JS contexts share a small number of runtime threads:

# Start a pool with N runtime threads (defaults to scheduler count)
{:ok, pool} = QuickBEAM.ContextPool.start_link(name: MyApp.JSPool, size: 4)

# Each context is a GenServer with its own JS global scope
{:ok, ctx} = QuickBEAM.Context.start_link(pool: MyApp.JSPool)
{:ok, 3} = QuickBEAM.Context.eval(ctx, "1 + 2")
{:ok, "HELLO"} = QuickBEAM.Context.eval(ctx, "'hello'.toUpperCase()")
QuickBEAM.Context.stop(ctx)

Contexts support the full API — eval, call, Beam.call/callSync, DOM, messaging, browser/node APIs, handlers, and supervision:

# In a Phoenix LiveView
def mount(_params, _session, socket) do
  {:ok, ctx} = QuickBEAM.Context.start_link(
    pool: MyApp.JSPool,
    handlers: %{"db.query" => &MyApp.query/1}
  )
  {:ok, assign(socket, js: ctx)}
end

The context is linked to the LiveView process — it terminates and cleans up automatically when the connection closes. No explicit terminate callback needed.

Granular API groups

Contexts can load individual API groups instead of the full browser bundle:

QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :fetch])  # 231 KB
QuickBEAM.Context.start_link(pool: pool, apis: [:beam, :url])    # 108 KB
QuickBEAM.Context.start_link(pool: pool, apis: false)            #  58 KB
QuickBEAM.Context.start_link(pool: pool)                         # 429 KB (all browser APIs)

Available groups: :fetch, :websocket, :worker, :channel, :eventsource, :url, :crypto, :compression, :buffer, :dom, :console, :storage, :locks. Dependencies auto-resolve.

Per-context resource limits

{:ok, ctx} = QuickBEAM.Context.start_link(
  pool: pool,
  memory_limit: 512_000,      # per-context allocation limit (bytes)
  max_reductions: 100_000      # opcode budget per eval/call
)

# Track per-context memory
{:ok, %{context_malloc_size: 92_000}} = QuickBEAM.Context.memory_usage(ctx)

Exceeding memory_limit triggers OOM. Exceeding max_reductions interrupts the current eval but keeps the context usable for subsequent calls.

API surfaces

QuickBEAM can load browser APIs, Node.js APIs, or both:

# Browser APIs only (default)
QuickBEAM.start(apis: [:browser])

# Node.js compatibility
QuickBEAM.start(apis: [:node])

# Both
QuickBEAM.start(apis: [:browser, :node])

# Bare QuickJS engine — no polyfills
QuickBEAM.start(apis: false)

Node.js compatibility

Like Bun, QuickBEAM implements core Node.js APIs. BEAM-specific extensions live in the Beam namespace.

{:ok, rt} = QuickBEAM.start(apis: [:node])

QuickBEAM.eval(rt, """
  const data = fs.readFileSync('/etc/hosts', 'utf8');
  const lines = data.split('\\n').length;
  lines
""")
# => {:ok, 12}

| Module | Coverage | |---|---| | process | env, cwd(), platform, arch, pid, argv, version, nextTick, hrtime, stdout, stderr | | path | join, resolve, basename, dirname, extname, parse, format, relative, normalize, isAbsolute, sep, delimiter | | fs | readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, lstatSync, unlinkSync, renameSync, rmSync, copyFileSync, realpathSync, readFile, writeFile | | os | platform(), arch(), type(), hostname(), homedir(), tmpdir(), cpus(), totalmem(), freemem(), uptime(), EOL, endianness() |

process.env is a live Proxy — reads and writes go to System.get_env / System.put_env.

Resource limits

{:ok, rt} = QuickBEAM.start(
  memory_limit: 10 * 1024 * 1024,  # 10 MB heap
  max_stack_size: 512 * 1024        # 512 KB call stack
)

Introspection

# List user-defined globals (excludes builtins)
{:ok, ["myVar", "myFunc"]} = QuickBEAM.globals(rt, user_only: true)

# Get any global's value
{:ok, 42} = QuickBEAM.get_global(rt, "myVar")

# Runtime diagnostics
QuickBEAM.info(rt)
# %{handlers: ["db.query"], memory: %{...}, global_count: 87}

Bytecode disassembly

Disassemble QuickJS bytecode into structured Elixir terms — like :beam_disasm for the BEAM:

{:ok, bc} = QuickBEAM.disasm(rt, "function fib(n) { if (n <= 1) return n; return fib(n-1) + fib(n-2) }")
fib = hd(bc.cpool)

fib.name       # "fib"
fib.args       # ["n"]
fib.stack_size # 4
fib.opcodes
# [
#   {0, :get_arg0, 0},
#   {1, :push_1, 1},
#   {2, :lte},
#   {3, :if_false8, 7},
#   {5, :get_arg0, 0},
#   {6, :return},
#   {7, :get_var, "fib"},
#   {12, :get_arg0, 0},
#   {13, :push_1, 1},
#   {14, :sub},
#   {15, :call1, 1},
#   ...
# ]

disasm/1 works on precompiled bytecode binaries without a runtime:

{:ok, bytecode} = QuickBEAM.compile(rt, source)
# later, even on a different node:
{:ok, %QuickBEAM.Bytecode{}} = QuickBEAM.disasm(bytecode)

DOM

Every runtime has a live DOM tree backed by lexbor (the C library behind PHP 8.4's DOM extension and Elixir's fast_html). JS gets a full document global with spec-compliant prototype chains (instanceof HTMLElement works), node identity (el.parentNode === el.parentNode), and uppercase tagName for HTML elements:

document.body.innerHTML = '<ul><l
View on GitHub
GitHub Stars330
CategoryDevelopment
Updated7h ago
Forks8

Languages

C

Security Score

95/100

Audited on Apr 6, 2026

No findings