bkjbbs: A Node.js Packet Radio BBS Library for Direwolf

bkjbbs: A Node.js Packet Radio BBS Library for Direwolf

Packet radio is one of those ham radio modes that never really died, it just went quiet in a lot of places. The infrastructure aged out, the BBS software stopped being maintained, and the Linux AX.25 kernel stack was deprecated. But the protocol is still sound, the radios are still on the desk, and a new generation of operators is quietly rebuilding the network with modern tools.

A good example landed on r/PacketRadioRedux in late July 2026. Reddit user u/boredbullet, who identifies as Jakub Fedyczak (callsign SP6BKJ, running SR6BBS at 5W from grid JO81MB), published a Node.js library called bkjbbs that lets you run your own packet BBS using Direwolf as the TNC. It is live on the air right now, tested with both the legacy Linux axcall tool and the iOS Packet Commander app, and it is a clean, modern, TypeScript-typed alternative to the creaking FBB/BPQ stack of the 1990s.

The library is on npm today. The author plans to open-source the full source once the code is tidied up.

Last updated: July 2026.


What bkjbbs Is

bkjbbs is a packet radio BBS, KISS, and AX.25 library for Node.js. It is published as an ES module with TypeScript declarations, MIT licensed, and the current version is 1.2.2. The author is Jakub Fedyczak.

The library does three things in one package:

  1. KISS transport. Connects to a KISS TNC over TCP (Direwolf’s KISS-over-TCP mode) or over a serial port (a hardware TNC or a Digirig Mobile). TCP KISS works with zero additional dependencies. Serial KISS needs the optional serialport peer dependency.
  2. AX.25 frame encoding and decoding. Full AX.25 layer 2: address fields with callsign and SSID, digipeaters, I frames (connected mode), UI frames (unnumbered information), supervisory frames (RR, RNR, REJ), and unnumbered frames (SABM, UA, DISC, DM). Modulo-8 sequence numbers, the C bit, the P/F bit, PID, and raw bytes are all handled.
  3. BBS session management. Accepts incoming SABM connections, runs a per-peer session with a configurable send window (1-7 unacknowledged I frames), retransmit timeout, and retry limit, and exposes a simple async API for sending and receiving text lines.

The quick start from the README tells you everything you need to see about the API design:

import { BBS } from "bkjbbs";

const bbs = new BBS({
  kiss: {
    tcp: { host: "localhost", port: 8001 },
  },
  bbs: {
    callsign: process.env.BBS_CALLSIGN ?? "N0CALL",
  },
  async onSession(session) {
    await session.sendLine("Hello from my BBS!");
    const line = await session.readLine();
    await session.sendLine(`You wrote: ${line}`);
    await session.disconnect();
  },
});

await bbs.start();

That is a complete, on-the-air BBS in 15 lines. Point it at a running Direwolf instance on port 8001, set your callsign, and you are accepting AX.25 connections.


The Hardware Stack Behind SR6BBS

The author is running SR6BBS on a real, operational station, and the hardware choices are worth noting because they are reproducible on a budget:

  • Server: Raspberry Pi Zero 2 W running Alpine Linux in diskless mode. Diskless means the OS runs from RAM, which is a smart choice for a headless radio node. No SD card wear, no filesystem corruption from power cycles, and the whole configuration is defined by a single overlay file you can version-control.
  • TNC: Digirig Mobile. A small USB soundcard-plus-PTT interface that shows up as a serial port and an audio device. It works with Direwolf out of the box.
  • Radio: Yaesu FTM-200D at 5W. A modern VHF/UHF mobile transceiver with a data port.
  • Server software: Direwolf acting as the software TNC, exposing a KISS TCP server on port 8001, with bkjbbs connecting to it as the BBS application.
  • Grid: JO81MB.

For clients, the author uses a Kenwood TH-D74 (which has a built-in TNC and can do packet radio standalone) and other handhelds via a Mobilinkd TNC. The TH-D74 connects to iOS over Bluetooth using a B.B. Link clone, so the HT works directly with the Packet Commander app on the phone with no laptop in the middle.

That is a complete, modern packet radio BBS stack for under $200 in hardware (excluding the radio), and the radio is something you probably already own.


The API in More Detail

The library is structured in three layers, and you can use whichever one matches your needs.

BBS Layer

The BBS class is the top level. You configure it with a KISS transport (TCP or serial), a callsign, and a set of event handlers. The key handler is onSession(session), which fires after an incoming SABM is accepted and the UA response is sent. Each peer/port pair gets an independent session.

The session API is async and straightforward:

  • session.sendLine(text) sends a line with the configured line ending (default \r).
  • session.readLine() returns a promise that resolves with the next received line.
  • session.waitForInput() returns the next raw I-frame information field as a Buffer, for binary or non-line-oriented protocols.
  • session.onMessage(callback) registers a synchronous callback for every received I-frame.
  • session.disconnect() starts a graceful local disconnect, waiting for outstanding I frames to be acknowledged before sending DISC.

Sends are serialized and observe the configured AX.25 send window (default 7, max 7 per the modulo-8 spec). If the window is full, the send promise waits for acknowledgements. If the session closes, pending reads and blocked sends reject with a clear error message.

BBS options you can tune:

Option Default Description
callsign required Local AX.25 callsign, validated and uppercased
lineEnding \r Text appended by sendLine()
retransmitTimeoutMs 15000 Delay before connected-mode ack recovery starts
maxRetries 3 Max recovery/retransmit attempts
sendWindow 7 Max unacknowledged I frames (1-7)

AX.25 Layer

Below the BBS is a complete AX.25 frame encoder and decoder. The exports include:

  • parseCallsign(value) and formatCallsign(callsign) for callsign validation and formatting.
  • encodeAddress(value, options) and decodeAddress(buffer, offset) for the 7-byte AX.25 address field, including the C bit and address-extension bit.
  • encodeFrame(frame) as the dispatcher, plus specific encoders: encodeUIFrame, encodeIFrame, encodeSupervisoryFrame, encodeUnnumberedFrame.
  • decodeFrame(buffer) for decoding any received AX.25 frame, including destination, source, digipeaters, C bits, P/F bit, sequence numbers, PID, info bytes, and raw bytes.
  • Constants: CONTROL_UI (0x03), CONTROL_DM (0x0f), CONTROL_SABM (0x2f), CONTROL_DISC (0x43), CONTROL_UA (0x63), PID_NO_LAYER_3 (0xf0).

This is a proper, spec-aware AX.25 implementation, not a thin wrapper. If you wanted to build something other than a BBS (a digipeater, a beacon, an APRS i-gate, a custom application), the AX.25 layer is there for you to build on.

KISS Layer

At the bottom is a KISS TNC framing layer, grouped under the kiss namespace:

  • kiss.encodeFrame(payload, options) encodes and escapes one complete KISS frame.
  • kiss.decodeFrames(buffer) decodes all complete frames in a buffer, returning a remainder for incomplete trailing data.
  • kiss.Decoder is a stateful streaming decoder for input split across multiple chunks, with a write(buffer) method and a remainder property.
  • Constants: kiss.COMMAND_DATA (0x00), kiss.FEND (0xc0), kiss.FESC (0xdb), kiss.TFEND (0xdc), kiss.TFESC (0xdd).

Custom Transports

If TCP and serial are not enough, you can supply a custom transport through the second BBS constructor argument. The transport interface is four methods:

interface Transport {
  onPacket(callback: (packet: kiss.Packet) => void | Promise<void>): () => void;
  connect(): Promise<void>;
  close(): Promise<void>;
  sendPacket(packet: kiss.Packet): Promise<void>;
}

This means you could write a transport for a network TNC protocol, a Bluetooth TNC, or a mock transport for testing, and the BBS, AX.25, and KISS layers all work unchanged.


Why This Matters for Packet Radio

The packet radio community has a software problem. The classic BBS systems, FBB and JNOS and BPQ, are still around, but they are showing their age. The Linux AX.25 kernel stack was deprecated, which means axcall, ax25d, and the rest of the standard toolchain are running on borrowed time. Direwolf is excellent as a software TNC, but it does not give you a BBS by itself, it gives you KISS.

What bkjbbs does is fill the gap between Direwolf (the TNC) and a modern application layer. You get:

  • A BBS that runs on any platform Node.js runs on (Linux, macOS, Windows, Alpine, Raspberry Pi, even a phone with Termux if you wanted to).
  • An async, event-driven API that fits naturally into modern JavaScript applications. You can serve a BBS, log to a database, expose a web dashboard, bridge to MQTT or a chat platform, or integrate with Winlink-style message stores, all from the same process.
  • A clean AX.25 and KISS implementation that is independently useful for any packet radio project, not just BBS work.
  • TypeScript declarations throughout, so editor autocomplete and type checking work out of the box.

The fact that it is running live on the air as SR6BBS, tested against both legacy tools (axcall) and modern mobile clients (Packet Commander on iOS), means it is not a toy. It is interoperable with the existing packet radio ecosystem.


The iOS Packet Commander Connection

The author specifically recommends the iOS Packet Commander app, and the client-side setup is worth highlighting because it lowers the barrier to entry dramatically.

With a Kenwood TH-D74 (which has a built-in KISS TNC) and a B.B. Link clone Bluetooth adapter, the handheld talks directly to the iPhone over Bluetooth. Packet Commander on the iPhone handles the AX.25 connection and the BBS session. No laptop, no Pi, no serial cable in the field. You connect to SR6BBS from a handheld radio and a phone in your pocket.

For other handhelds without a built-in TNC, a Mobilinkd TNC provides the same KISS-over-Bluetooth bridge. This is the modern packet radio client story: a handheld, a Mobilinkd, and a phone app. The BBS on the other end can be bkjbbs on a Pi Zero.


Reproducing the SR6BBS Stack

If you want to build something similar, here is the recipe:

  1. Get a TNC interface. Either Direwolf on a Pi with a USB soundcard (like the Digirig Mobile), or a hardware KISS TNC like a Mobilinkd or a Kenwood TH-D74’s built-in TNC.
  2. Install Node.js. Any current LTS version. On Alpine Linux, apk add nodejs npm.
  3. Install bkjbbs. npm install bkjbbs. If you are using a serial TNC, also npm install serialport.
  4. Configure Direwolf. A minimal direwolf.conf for VHF 1200 baud:
    ADEVICE plughw:1,0
    CHANNEL 0
    MODEM 1200
    KISS TCP 8001

    Start it with direwolf -c direwolf.conf.

  5. Write your BBS script. Start with the quick-start example above. Set your callsign, set the KISS TCP host and port to match Direwolf, and implement your onSession handler.
  6. Connect to it. From a client (Packet Commander on iOS, axcall on Linux, another Direwolf instance, or any AX.25 terminal), connect to your BBS callsign.

The whole thing fits on a Raspberry Pi Zero 2 W with room to spare. If you want diskless operation like SR6BBS, Alpine Linux with an overlay file is the way to do it.


What Is Still To Come

The author has said the library will be released as open source once the code is tidied up. The npm package is already published and usable, but the source repository is not yet public. When it is, it will be worth watching for:

  • Message store and forward (the classic BBS mailbox functionality).
  • Bulletin distribution and forwarding between BBS nodes.
  • YAPP or other file transfer protocol support.
  • Integration with the existing FBB/BPQ message network, if the author chooses to bridge to it.
  • A reference BBS application built on the library, so non-programmers can run one without writing JavaScript.

Even without those features, the library as it stands today is a solid foundation. The AX.25 and KISS layers alone are a contribution to the Node.js amateur radio ecosystem, and the BBS session API is clean enough that building a full-featured BBS on top of it is a weekend project, not a multi-month undertaking.


Why I Am Writing About This

Packet radio gets dismissed as a dead mode by people who have not looked at it recently. It is not dead. It is being rebuilt, quietly, by operators who want digital communications that do not depend on the internet, that work with $30 handhelds and $20 TNCs, and that interoperate with decades of existing protocol and practice. bkjbbs is exactly the kind of project that rebuild needs: modern, clean, well-documented, tested on the air, and built by someone who is running it for real.

If you have a Pi and a radio, give it a try. If you are looking for a Node.js AX.25 library for any project, this one is worth your attention. And if you want to see packet radio thrive, star the repo when it goes public and contribute if you can.

73, and see you on the BBS.


Sources and Further Reading

Post Comment