Features

The BBS is back. And it’s yours.

The Exchange isn’t a nostalgia project. It’s a real-time, federated communication platform that happens to wear a terminal aesthetic. Every feature below exists for a reason — to make the BBS a living, breathing community platform that a sysop can shape without recompiling, restarting, or asking permission.


1. Phosphor Language

Every BBS before The Exchange forced you into one of two hells: edit a static config file and hope the parser doesn’t choke on a stray tab, or crack open the Java source, recompile, and restart the whole system to move a menu item. Phosphor is a third option — a domain-specific language designed for BBS behavior. It’s not config, and it’s not Java. It’s executable scripting that the system hot-loads.

The old way: Java recompile

You want to add a “File Area” option to the main menu. In a Java-based BBS, that’s:

// MainMenuScreen.java — edit, recompile, restart
public class MainMenuScreen extends Screen {
    @Override
    public void render(TerminalContext ctx) {
        ctx.println("[1] Message Boards");
        ctx.println("[2] Chat Lounge");
        // you add this line:
        ctx.println("[3] File Area");
        ctx.println("[Q] Quit");
    }

    @Override
    public void onInput(String input, TerminalContext ctx) {
        switch (input.toUpperCase()) {
            case "1" -> ctx.navigate("boards");
            case "2" -> ctx.navigate("chat");
            // you add this case:
            case "3" -> ctx.navigate("files");
            case "Q" -> ctx.navigate("logout");
        }
    }
}
// then: mvn package, kill the JVM, restart, pray nothing else broke

That’s a 15-minute round-trip — edit, compile, deploy, restart — for a single menu entry. Multiply by every screen, every board, every game, every theme. Classic BBS software dies here.

The Phosphor way

Same change, one file, no restart:

menu "main" {
    option "1" "Message Boards" -> boards
    option "2" "Chat Lounge"    -> chat
    option "3" "File Area"      -> files    // new line
    option "Q" "Quit"           -> logout
}

Save the file. The BBS picks it up on the next render cycle. No compile, no restart, no downtime. This is the fundamental shift: the BBS is editable while it runs.

Phosphor covers the full surface area of the system — menus, boards, screens, themes, door games, mail filters, federation rules. It’s a single language for everything, not a patchwork of YAML, JSON, Lua, and shell scripts held together with duct tape.

// bbs.phos — the whole system, one file if you want
board "General" {
    description = "General discussion — anything goes"
    read  = ["users"]
    write = ["users"]
    federated = true
    max_msg_size = 4096
    on post {
        if msg.length > max_msg_size {
            reject("Message exceeds {max_msg_size} bytes")
        }
    }
}

screen "welcome" {
    render {
        ansi_gradient("THE EXCHANGE", colors: ["red", "yellow", "cyan"])
        println("")
        println("Connected as {user.name} — last login {user.last_login}")
        println("{stats.online_users} users online across {stats.federated_nodes} nodes")
    }
}

2. Dual Terminal + Web

The Exchange runs two front-ends off one backend. SSH for the terminal purists. WebSocket for the browser users. Both connect to the same event bus, read the same database, and see the same users. There is no “terminal version” and “web version” — there’s one BBS with two doors.

                    ┌─────────────────────────────┐
                    │      Reactive Event Bus     │
                    │   (Postgres + WebSockets)   │
                    └──────────┬─────────┬────────┘
                               │         │
              ┌────────────────┘         └────────────────┐
              │                                               │
     ┌────────▼────────┐                          ┌─────────▼─────────┐
     │  SSH / Telnet   │                          │  WebSocket / Web   │
     │  (jterm)        │                          │  (browser client)  │
     │  ANSI terminal  │                          │  HTML + CSS render │
     └─────────────────┘                          └───────────────────┘

A chat message typed in an SSH session in New York hits the event bus, fans out to every connected client — terminal and web alike — and appears in a browser tab in Berlin within the same frame. The web client isn’t a separate app with a sync delay. It’s a render target on the same bus.

// This handler runs identically for both SSH and web clients
on chat_message(room: "lounge") {
    broadcast(room, {
        user: msg.user,
        text: msg.text,
        timestamp: now(),
        // web clients get this as JSON over WebSocket
        // terminal clients get this as ANSI-formatted text
    })
}

No bridge. No translation layer. No “sync job” that runs every 30 seconds. The event bus is the source of truth, and both transports subscribe to it directly.


3. Federation

A BBS shouldn’t be an island. The Exchange federates — multiple independent nodes sync boards, mail, and events across the internet via jnet, the federation transport. A post on the Bronxville node shows up on the Ohio node in real-time. Go offline for a week? You get backfill on reconnect, not a gap.

Events, not entities

Most federated systems sync entity rows — they replicate the database table and hope timestamps sort everything out. The Exchange syncs events. An event is “user X posted message Y to board Z at time T.” The node replays events into its own state. This means:

Federated board declaration

board "Trade Post" {
    description = "Buy, sell, trade — federated across all nodes"
    read  = ["users"]
    write = ["users"]
    federated = true

    // which nodes replicate this board
    peers = ["bronxville.exchange", "ohio.exchange", "pdx.exchange"]

    // events propagate; entities stay local
    sync_mode = "eventual"
    conflict  = "last_write_wins"

    on post {
        // fires on every node that replicates this board
        emit("board.posted", {
            board: "Trade Post",
            msg_id: msg.id,
            node: local_node()
        })
    }
}
// jnet node identity — declared once per instance
node "bronxville.exchange" {
    bind    = "0.0.0.0:9400"
    peers   = ["ohio.exchange", "pdx.exchange"]
    // reconnect behavior
    backfill = true          // pull missed events on reconnect
    backfill_window = "30d"  // up to 30 days of history
}

When Bronxville loses its uplink, it queues events locally. When the link comes back, jnet replays the queue and pulls backfill from peers. Users on the disconnected node kept posting — their messages land on the network when connectivity returns. No data loss, no manual reconciliation.


4. Reactive Everything

Screens in The Exchange are live views, not snapshots. There’s no refresh key. There’s no polling loop. There’s no “log out and back in to see new mail.” The UI binds to reactive state, and when the state changes, the screen updates in the same frame.

Three primitives:

Reactive inbox

screen "inbox" {
    // bind the message list to the user's mail state
    // this updates live — no refresh, no polling
    bind messages = user.inbox

    // derived value: unread count recalculates when messages change
    computed unread = messages.filter(m -> !m.read).count()

    // when new mail arrives, the list updates AND we flash a notice
    on mail_received {
        flash("New mail from {msg.from}")
        // the bind above already re-rendered the list — nothing else to do
    }

    render {
        ansi_gradient("INBOX", colors: ["cyan", "blue"])
        println("{unread} unread / {messages.count()} total")
        println("─────────────────────────────────")
        for m in messages {
            marker = m.read ? " " : "●"
            println("{marker} [{m.date}] {m.from:20} {m.subject}")
        }
    }
}

The bind is the contract. When user.inbox mutates — because a mail event arrived from another node, another user, or the local system — the screen re-renders. The sysop didn’t write a poller. The user didn’t press F5. The framework handled it.

This extends everywhere: board feeds update when posts land, the user list refreshes when someone connects, the chat scrollback grows in real-time. Reactive is the default, not an opt-in.


5. User Scripting Sandbox

In the 90s, BBS users wrote their own door games, macros, and auto-responders. The Exchange brings that back — safely. Every user can write user_handler blocks in Phosphor. These run in a per-session sandbox: the user can read and modify their own state (mail, settings, session vars) but cannot touch other users, the filesystem, the system config, or other sessions.

Mail auto-filter

// written by a regular user, not a sysop
// runs only for this user's session
user_handler on mail_received {
    // route mail from known senders into folders
    if msg.from == "boss@work.exchange" {
        mail_move(user.id, msg.id, "Work")
        flash("Work mail filed")
        return
    }

    // auto-delete spam by keyword
    if msg.subject contains "viagra" or msg.subject contains "lottery" {
        mail_delete(user.id, msg.id)
        return
    }

    // auto-reply when away
    if user.flag("away") and !msg.from.startswith("noreply") {
        mail_send(
            to: msg.from,
            subject: "Re: {msg.subject}",
            body: "I'm away until {user.flag('away_until')}. Will reply then."
        )
    }
}

What users CAN do

What users CANNOT do

The sandbox is enforced at the interpreter level, not by convention. A user handler that tries mail_delete(other_user.id, ...) gets a runtime error, not a deleted message.


6. ANSI Art Pipeline

The terminal aesthetic isn’t a CSS theme — it’s generated art. The Exchange ships a full ANSI pipeline for programmatic art creation:

Welcome banner with gradient

screen "login" {
    render {
        // 3-stop gradient across the banner text
        ansi_gradient("THE EXCHANGE", colors: ["#ff0066", "#ffaa00", "#00ddff"], style: "block")

        // a bordered info panel
        panel(border: "double") {
            println("  Node: {local_node()}")
            println("  Users online: {stats.online_users}")
            println("  Federation: {stats.federated_nodes} peers")
        }

        // convert a logo image to ANSI, center it
        art = image_to_ansi("assets/logo.png", width: 60, colors: "256")
        println(center(art))

        // animated ticker at the bottom — updates every 2s
        ansi_scroll(motd, speed: 2, style: "marquee")
    }
}

Image-to-ANSI in a door game

door "dungeon" {
    room "throne" {
        on enter {
            // render a PNG as ANSI art for the room scene
            scene = image_to_ansi("scenes/throne.png", width: 70, dither: "floyd")
            println(scene)
            println("The throne room. Dusty. A crown glints on the seat.")
        }
    }
}

Themes can shift based on time of day, user preference, or game state. The art is generated at render time, not pre-baked — change the gradient colors or the source image and the next frame is different.


7. In-BBS Editor

Sysops shouldn’t need SSH to edit the BBS. The Exchange has a full-screen Phosphor editor inside the terminal — syntax highlighting, auto-complete, live parse feedback, and save-to-apply. You connect to your own BBS, hit the editor key, and change the system live.

What the editor does

// The editor experience, conceptually:
// [F2] Save & Apply   [F3] Open File   [F4] Jump to Symbol   [Ctrl-Q] Quit
//
//  ┌─ bbs.phos ──────────────────────────────────── 12:04 ─┐
//  │ 1  board "General" {                                    │
//  │ 2      description = "General discussion"               │
//  │ 3      read  = ["users"]                                │
//  │ 4      write = ["users"]                                │
//  │ 5      federated = true                                 │
//  │ 6      on post {                                       │
//  │ 7          if msg.length > 4096 │ ← undeclared 'msg'   │
//  │ 8              reject("Too long")                       │
//  │ 9          }                                            │
//  │10  }                                                    │
//  └────────────────────────────────────────────────────────┘
//  Status: 1 error (line 7) — 'msg' not in scope in 'on post'

The error on line 7 appears while you’re typing, not after you save. Fix it, save, and the board definition is live. The edit-deploy-verify loop collapses to “edit, save, done.”

No SSH. No SCP. No git push and CI build. No Docker rebuild. You edit the BBS from the BBS.


8. Door Games in Phosphor

Door games were the soul of 90s BBSes — Tradewars, Legend of the Red Dragon, Lunatix. The Exchange brings them back, written in the same language as the BBS itself. No separate runtime, no external process, no shared-memory hacks. A door game is just a door block in Phosphor.

Simple room declaration

door "dungeon" {
    title "The Caverns of Bronxville"
    max_players = 1
    save_state = true    // persists per-user between sessions

    // global state — survives across rooms and sessions
    state {
        room = "entrance"
        hp = 100
        gold = 0
        inventory = []
    }

    room "entrance" {
        on enter {
            ansi_gradient("CAVE ENTRANCE", colors: ["gray", "dark_gray"])
            println("You stand at the mouth of a cave. Cold air.")
            println("Exits: [n] north (dark tunnel), [s] south (back to town)")
        }
        on input "n" -> goto "tunnel"
        on input "s" -> goto "town"
    }

    room "tunnel" {
        on enter {
            println("Dark tunnel. You hear dripping.")
            if !state.inventory.contains("torch") {
                println("You can't see. You should find a torch.")
                return
            }
            println("Your torch reveals a chest to the [e]ast.")
        }
        on input "e" -> goto "chest_room"
        on input "w" -> goto "entrance"
    }
}

// Scoreboard — shared across all players on this node
scoreboard "dungeon" {
    metric = "gold"
    sort = "desc"
    display_top = 10
    federated = true   // syncs across nodes
}

What door blocks give you

Share a door game as a .phos file. Drop it in the modules directory, it’s playable. Like 90s door game packages, but open and hackable.


9. Modern Stack, Retro Soul

This isn’t a Python script running in a basement. The Exchange is an engineered system with a production-grade stack:

LayerTechnologyWhy
RuntimeJava 26Virtual threads, pattern matching, records. Real performance, real concurrency.
StoragePostgreSQLACID, JSONB, row-level security. The event bus is backed by Postgres logical replication.
Real-timeWebSocketsBrowser clients get push updates over a persistent WS connection. Same bus as terminal.
Event busReactive event busIn-process pub/sub backed by Postgres NOTIFY + jnet for cross-node propagation.
TerminaljtermA proper terminal framework — ANSI parsing, screen rendering, input handling, VT220 support. Not a raw socket hack.
FederationjnetEvent-based sync transport with backfill, vector clocks, and deterministic conflict resolution.

The reactive event bus is the spine. Terminal input, web input, mail delivery, board posts, federation events, and door game state changes all publish to the same bus. Subscribers — screens, handlers, jnet peers — react to events as they land. There’s no polling, no cron job scanning for changes, no “sync service” that runs every 60 seconds.

// The event bus is first-class in Phosphor
on "board.posted" {
    // fires for local posts and federated posts alike
    if event.board == "General" and event.user != local_user() {
        flash("{event.user} posted to General")
    }
}

// publish your own events
emit("custom.greeting", { from: user.name, room: "lounge" })

This stack scales. One node handles hundreds of concurrent terminal sessions. Federation lets you grow horizontally. Postgres gives you real durability — power loss doesn’t eat your message base.


10. Module Marketplace

The 90s BBS ecosystem thrived on shareable door games, ANSI packs, and mod scripts passed around on floppy disks and FidoNet. The Exchange brings that back with a module system and a community marketplace.

Install a module

phosphor install dungeon-game
phosphor install ansi-pack-retrowave
phosphor install mail-filters-pro

Modules are .phos files — boards, doors, screens, handlers, themes, art assets — packaged and shared. Install one and it drops into the live system. No recompile, no restart, no dependency hell.

Author and share

# package your module
phosphor pack my-door-game/ --name "tavern-quest" --version 1.0

# publish to the community registry
phosphor publish tavern-quest
// module.phos — module manifest
module "tavern-quest" {
    version = "1.0"
    author = "bronxville.exchange"
    description = "A multi-room tavern adventure with a bar fight mini-game"
    requires = ["phosphor >= 1.2"]
    provides = [
        door("tavern"),
        board("tavern-quest-discussion"),
        scoreboard("tavern-quest")
    ]
    config {
        difficulty = "medium"
        max_players = 4
    }
}

The marketplace is community-run. No corporate gatekeeper, no app store review board. Sysops publish, sysops install, sysops review. If a module is bad, the community says so. If it’s good, it propagates across the federation naturally.


11. Off the Grid + MeshCore LoRa

The Exchange runs on your server. Not AWS. Not Google Cloud. Not Big Tech infrastructure.

This is counter-culture infrastructure. The internet was supposed to be decentralized, community-owned, and free. The BBS scene was that — a network of independent nodes run by independent sysops, federated through FidoNet, accountable to their communities. Then Big Tech paved it over with walled gardens and engagement-optimized feeds.

The Exchange brings the old model back with modern engineering. You run a node. Your friend runs a node. Your community runs a node. They federate. Messages, boards, and games sync across the network. No central server owns the conversation. No corporation sits in the middle.

The BBS doesn’t just look different from the modern web. It is different. It’s a place you go on purpose, not a feed that comes to you.

MeshCore: Federation Without Internet

Here’s what no other BBS system has: LoRa mesh networking via MeshCore. The Exchange doesn’t just federate over the internet — it federates over radio.

MeshCore is a LoRa-based mesh networking protocol integrated into the BBS via jnet. Nodes sync boards, mail, and messages over long-range radio links — no internet required. A BBS in Bronxville syncs with a BBS in Ohio over a 900MHz LoRa link. No ISP. No cell tower. No WiFi. Just radio.

bbs "Bronxville Node" {
    node_name = "bronxville"
    federate_with = ["ohio", "florida"]

    // Internet federation (jnet over TCP)
    peer "ohio" {
        host = "ohio.boremaj.net"
        port = 9777
    }

    // Radio federation (MeshCore over LoRa)
    peer "florida" {
        transport = meshcore
        band = "915MHz"
        // LoRa: long range, low bandwidth, no internet needed
    }

    board "General" {
        federated = true   // syncs over BOTH internet and radio
    }
}

Why this matters:

MeshCore features:

The BBS doesn’t just look different from the modern web. It is different. It’s a place you go on purpose, not a feed that comes to you. And with MeshCore, it works even when the internet doesn’t.

// sovereignty is a one-liner
node "your-town" {
    bind  = "0.0.0.0:9400"
    peers = []  // start solo, add federation peers when you're ready
    // your rules. your data. your community.
    // add a LoRa peer and you don't even need the internet
}