{ } qjs-lws
QuickJS × libwebsockets

Network servers that fit where Node.js doesn't.

qjs-lws binds libwebsockets into QuickJS: HTTP/1.1 and HTTP/2, WebSocket, raw TCP and UDP, TLS — driven by QuickJS's own event loop, with a JavaScript layer that looks like the web platform you already know.

import { serve, Response } from './lib/serve.js';

serve({
  port: 8080,

  websocket: {
    open(ws)          { ws.subscribe('chat'); },
    message(ws, data) { ws.publish('chat', data); },
  },

  fetch(req, server) {
    const { pathname } = new URL(req.url);

    if(pathname === '/ws' && server.upgrade(req))
      return;

    return new Response('hello from qjs-lws');
  },
});

Why it exists

A production-grade C networking stack and a very small interpreter, joined so that neither one has to pretend to be the other.

No service loop

The binding hooks libwebsockets' pollfd callbacks straight into QuickJS's own os.setReadHandler and os.setWriteHandler. No lws_service() spun from JS, no polling tax — your script's normal event loop drives the stack. An optional epoll(7) backend collapses it to a single fd.

Every role, one context

HTTP client and server, WebSocket client and server, raw TCP and UDP, static-file mounts, redirects, and TLS with peer-certificate introspection — all from one LWSContext, on one port if you want it that way.

APIs you already know

fetch(), WebSocket, URL/URLSearchParams, Headers, Request, Response, FormData, WHATWG Streams, AbortController, TCPSocket. Written from the specs, not approximated.

Bun-shaped serve()

serve({ fetch, websocket }) with server.upgrade(), WebSocket pub/sub topics, chunked streaming and backpressure — close enough to Bun that most handlers port across unchanged.

Routing and middleware

An Express-compatible app/Router layer when you want one: path params, method dispatch, sub-routers, sessions, multipart bodies, MIME types. Optional — the primitives underneath stay reachable.

Drop to C-level control

The wrappers are a convenience, never a wall. Every callback reason libwebsockets emits is reachable as an onEventName handler, with the raw wsi in hand.

Built for the small end

Where a Node or Bun runtime is the largest thing on the device, and the job is one well-defined piece of networking.

Embedded & appliance firmware

A scriptable control plane on hardware with a flash budget: config UI, telemetry push over WebSocket, firmware upload through the native multipart parser.

Small utility servers

Health endpoints, webhook receivers, metrics scrapers, LAN dashboards — the services that never justified a node_modules tree.

Gateways & proxies

Terminate TLS, translate protocols, bridge WebSocket to raw TCP. Mixed HTTP and raw listeners on one vhost make protocol-sniffing front doors straightforward.

Two altitudes, same stack

Native — you hold the wsi

Protocol handlers map one-to-one onto struct lws_protocols.

import { createServer, LWSMPRO_NO_MOUNT } from 'lws';

createServer({
  port: 8080,
  vhostName: 'localhost',
  mounts: [{
    mountpoint: '/chat',
    protocol: 'chat',
    originProtocol: LWSMPRO_NO_MOUNT,
  }],
  protocols: [{
    name: 'chat',
    onEstablished(wsi)   { console.log('open', wsi.peer?.host); },
    onReceive(wsi, data) { wsi.write(data); },
    onClosed(wsi)        { console.log('close'); },
  }],
});

Web-shaped — you hold a Request

The same connection, wrapped in the APIs a browser would hand you.

import { fetch } from './lib/fetch.js';

const res = await fetch('https://example.com/api', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ hello: 'world' }),
});

console.log(res.status, res.headers.get('content-type'));
console.log(await res.json());

Get it running

CMake builds the module and, if you let it, the vendored libwebsockets too.

git clone --recursive https://github.com/rsenn/qjs-lws.git
cd qjs-lws
mkdir build && cd build
cmake -DDEBUG_OUTPUT=OFF -DDO_TESTS=ON ..
make -j

# lws.so lands in build/; point QuickJS at it
qjs -I ./build ../tests/unittests/test-lwscontext.js

Getting started → Full build options

Where it stands

Honest status: qjs-lws is in daily use and the native API is stable, but it hasn't cut a versioned release yet — track the changelog and pin a commit. The web-platform layer is a deliberate subset: no CORS, cache, or service-worker semantics, because this runs server-side. Known gaps are written down rather than papered over — see the API compatibility report and the BUGS file.

Worth a look next