All Articles

bxThreads: a bx-express port of DismalThreads

Ported my forum app (DismalThreads) from ColdBox/CBWIRE to BoxLang/boxlang-express as bxThreads — swapping CBWIRE's component reactivity for hand-rolled fetch()/websockets. Porting a real app with existing opinions (auth, rate limiting, realtime fan-out) is a better BoxLang stress test than starting fresh, since a blank project lets you dodge every hard problem by never hitting it.

I have been spending a lot of time with boxlang-express lately, trying to push the limits of what it can do. I have ported my blog to bx-express but that is a simple app. I wanted to try something with a lot more moving parts, so I decided to port DismalThreads, the Reddit-like forum I wrote in ColdBox using CBWire.

What was DismalThreads?

A CF/BoxLang-based forum system. The application itself has had quite a few iterations. I originally started back in 2023, but the database server died and took all the schema and data with it. Then, in 2024, I started recreating the schema from memory to get the system up and running again. BoxLang released at Into the Box 2025, and I eventually migrated fully to BoxLang. I put the website online in early 2026 and it ran until I eventually shut it down in August. It worked great, but it never really gained any momentum.

What bxThreads is (and why port instead of build new)

Same shape as DismalThreads: forums, posts (text or link), nested comments, votes, notifications, achievements, moderation. Server-rendered, fetch() for interactivity, no client framework underneath. Votes, comments, and notifications propagate over a STOMP websocket layer — two tabs on the same thread stay in sync without a refresh.

Stack:

  • BoxLang — language/runtime
  • boxlang-express — routing and middleware, Express-shaped, which made this feel more like translation than rewrite
  • MySQL — storage
  • Plain .bxm templates through a small custom view layer — no built-in layout system
  • Hand-rolled DI registry (lib/AppContext.bx) instead of Wirebox

I ported instead of building fresh because a blank project lets you dodge every hard problem by never running into it. DismalThreads already had opinions baked in — nested comment trees, session auth, realtime fan-out, rate limiting, CSRF — and reproducing each one on new infrastructure is what actually shows you where BoxLang and boxlang-express hold up and where they don't yet.

Reactivity: CBWIRE vs. fetch() + websockets

The two ports land on genuinely different models here, not just a server engine swap.

DismalThreads leaned on CBWIRE's HTML-over-the-wire approach: components declare state server-side, and CBWIRE handles the diffing and re-render over the wire. You write CFML, not JS, and the framework owns the synchronization. Convenient, but you're inside its component lifecycle and its opinions about what a "component" is.

bxThreads doesn't have anything like that — there's no component reactivity layer at all. Interactivity is hand-rolled fetch() calls hitting boxlang-express routes, plus a STOMP websocket layer for the stuff that needs to push rather than pull (votes, new comments, notifications landing across open tabs). It's more wiring, but also more legible: nothing is diffing a virtual DOM behind your back, you know exactly what triggers what.

Net effect: CBWIRE bought convenience at the cost of a framework abstraction between you and the DOM. The bxThreads approach is more manual but has no hidden machinery — the tradeoff is boilerplate for control.

Try it yourself

git clone https://github.com/robertz/bxthreads
cd bxthreads
boxlang setup.bxs
boxlang --bx-config ./boxlang.json app.bxs

setup.bxs does three things, in order, and it's safe to re-run: install dependencies, configure .env, and bootstrap the database — each step skips itself if there's nothing to do.

Dependencies. It reads box.json and installs every module into boxlang_modules/ via install-bx-module --local — not box install, since these modules are pinned to local installs rather than the registry. Anything already on disk gets skipped, so re-running setup after a git pull only fetches what's new.

Environment. It walks through every value in .env.example — port, site URL, the X-Api-Key used by the admin API, MySQL host/port/database/user/password — and writes .env. If a .env already exists, its values become the defaults, so you can hit Enter through the whole thing to leave it alone, or re-run setup just to add one new key without retyping everything else. The database name gets validated against ^[A-Za-z0-9_]+$ before it's accepted, since it's interpolated straight into the generated schema SQL.

Database. It checks whether the configured database already exists on the configured server. If it does, setup leaves it untouched — this step never touches existing data. If it doesn't, it asks for confirmation, then applies db/schema.sql (with the database name substituted in) and seeds an admin/admin login with RENEGADE_ADMIN permission. Change this password before the instance is reachable from anywhere but localhost — it's a bootstrap credential, not a default you want sitting on a public deployment.

The password hash is generated by spawning a fresh boxlang subprocess against the project's own boxlang.json — the setup script's own process booted before bx-password-encrypt was installed in step one, so it can't see BCryptHash() yet; a subprocess with a clean module resolution path sidesteps that.

Anything that fails — a missing mysql CLI, a module that won't install, a database connection refused — gets reported by name at the end rather than aborting the whole run, so you can fix one thing and re-run instead of starting over.

That gets you dependencies installed, .env populated, schema applied, and an admin/admin account waiting — boxlang --bx-config ./boxlang.json app.bxs then serves on http://localhost:3000 (or whatever port you gave it).

No comments yet — be the first.