Page Sysop: The Classic BBS Ritual Gets a Phone Push
September 16, 2026
Every classic BBS had a ritual for I need a human right now: page the sysop. It was the doorbell of the system — one keypress, a line of text, and somewhere in the house a person heard a chime and knew someone was waiting. Legacy boards grew it organically because they had a specific human property: the sysop was a person in a room, not a support ticket queue.
Phosphor has that again. Here’s the design, and the one bug we hit shipping it.
The user side: one keypress
From the main menu, [Y] Page Sysop opens a reason dialog. Type a reason — or don’t, and the page still goes out (“No reason given” is a valid state of affairs; sometimes the message is the urgency). Submit and the page is persisted immediately.
Persistence matters. A page isn’t a chat message that evaporates when nobody’s looking — it lands in a page_requests table with the sender, the reason, timestamps, and a claim state. If nobody’s online when you page, the request is waiting, not lost:
CREATE TABLE IF NOT EXISTS bbs.page_requests (
id BIGSERIAL PRIMARY KEY,
from_user VARCHAR(64) NOT NULL,
from_session_id VARCHAR(64),
reason TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
claimed_by VARCHAR(64),
claimed_at TIMESTAMPTZ,
handled_at TIMESTAMPTZ
);
Claiming uses a conditional UPDATE ... WHERE claimed_by IS NULL — the row version of “first sysop to the doorbell wins.” Two sysops on duty at the same time can’t double-claim; the loser’s update simply matches zero rows.
The sysop side: a popup that follows you around
The moment a page is published, a PageSysopMonitor subscribed to the sysop-page chat channel checks every active session for sysop/admin group membership and pops a modal alert on each one — sender excluded, so paging doesn’t ring your own bell. The alert is a real TUI dialog: Enter claims the page and opens a reply dialog; your reply lands as a DM popup on the sender’s live screen. Escape dismisses silently and the page stays in the table as unclaimed, so it isn’t lost — it’s just quiet.
The reply path reuses the chat bus’s DM channel naming — dm:a:b, alphabetically ordered — which means replies and their delivery popups ride infrastructure that already existed. The page system is a thin layer over the bus, not a parallel message network.
And then your phone rings
The modern twist: when a page arrives, the sysop also gets a push notification on the Phosphor iOS app — title 📞 Page from <user>, body the reason — via the server’s existing APNs stack. This is the piece that makes a home-run DTP server genuinely administrable: you don’t have to be watching the terminal to be reachable. You just have to be carrying your phone.
That’s the difference between a community server you check and one you inhabit. The third place should be able to reach you without owning your attention — and a page is the polite version of that. It’s opt-out, guarded by sysop.page.push, and it no-ops cleanly when no device is registered or APNs is disabled. The in-TUI alert and the persisted request are the floor; the push is a convenience layered on top.
We wired it through the same PushHook the mail system uses — onMessageSent(recipient, title, body, targetScreen, category) — rather than building a parallel notification path. One hook chain, one device registry, one preference system.
The bug that made it a story
The page published, the popup appeared, the row persisted — and the phone stayed silent. No error. The logs showed nothing but a swallowed warning.
Root cause: the push system resolves recipient preferences through a notification-prefs layer that only knows three categories — mail, chat_dm, chat_mention. Our new category, page, wasn’t registered anywhere. The delivery chain looked up preferences for page, found it was an unknown category, and quietly refused. Silent rejection by design; the failure mode was correct, just invisible.
The fix is a nice study in shipping schema changes safely: register page in the category list, widen the CHECK constraint in both dialects’ schema files, and add an idempotent startup migration that probes whether page is accepted and rebuilds the constraint if not — so existing installs converge to the new constraint on next boot without manual SQL. Fresh installs get the wide constraint from the DDL; old installs migrate themselves. Same constraint name on both paths.
The takeaway
A page is a small feature — one table, one monitor, one dialog, one hook call. But it’s the feature that makes the sysop present rather than merely available, which is exactly the texture a digital third place needs. The chime at the door is what made the old boards feel inhabited. We kept the ritual and gave it a modem that reaches your pocket.
Full post: https://phosphorbbs.net/blog/page-sysop-the-classic-bbs-ritual-with-a-modern-twist/
#BBS #terminal #digitalthirdplace #dtp #socialterminal #opensource #jterm #push