---
title: 'TTG Wiki BBS'
url: 'https://wiki.teletypegames.org/services/wiki-bbs'
markdown: 'https://wiki.teletypegames.org/services/wiki-bbs.md'
lang: en
date: '2026-11-01'
description: 'The Teletype Games wiki over telnet: it browses this wiki live through its JSON API, arranging the pages into pageable, searchable BBS menus. A supervisor login also opens the private pages. Live at teletypegames.org:2325, in the browser on bbsweb, and through TTG Gate BBS Overview A Ruby app on …'
taxonomy:
  tag:
    - howto
---

# TTG Wiki BBS

> The Teletype Games wiki over telnet: it browses this wiki live through its JSON API, arranging the pages into pageable, searchable BBS menus. A supervisor login also opens the private pages.
> 
> **Live at `teletypegames.org:2325`**, in the browser on [bbsweb](https://bbsweb.teletypegames.org), and through [TTG Gate BBS](https://wiki.teletypegames.org/services/ttg-gate-bbs)

# Overview

A Ruby app on the [**rubbs**](https://git.teletypegames.org/engines/rubbs) gem, like the [Teletype BBS](https://wiki.teletypegames.org/services/teletype-bbs). It has its own repository, `bbs/wiki-bbs`, and its own stack, and reads the [wiki](https://wiki.teletypegames.org/services/wiki-site) over the `interstack` network. It has two faces on one port. A call starts with rubbs' `BBS::Terminal` question: the board waits up to 1.5 seconds for the telnet answers, and a window wider than 40 columns is a modern terminal; 40 columns, or no answer at all (a C64 behind a WiFi modem speaks plain TCP), is taken for a Commodore. `1` picks the modern face, `2` the C64, RETURN takes the guess.

# The modern face

A full-screen, Norton Commander-style two-pane browser: the page tree on the left (indented by route depth), the selected page rendered as ANSI markdown on the right. The browser opens maximized; F5 / Alt-Enter turns it into a window and back.

| Key | Action |
|---|---|
| Up / Down | Pick a page (the right pane reloads) |
| Tab | Move the focus to the reader pane |
| PgUp / PgDn / Home / End | Scroll the reader |
| F2 | Reopen the browser |
| F5 / Alt-Enter | Window / full screen |
| F8 | Refresh the wiki index (clears the page cache) |
| F10 | Menubar |
| Alt-X / Esc | Quit / close the window |

Pages marked `*` are private (login-only on the wiki); **Wiki > Supervisor login** unlocks them, the same item locks them back. The mouse works too (xterm SGR), wheel scroll included.

# The C64 face

PETSCII on a 40×25 screen. The same page tree, one level per screen, as a numbered cursor menu of rubbs (`BBS::Selector`): a page with pages under it is marked `>`, a private one `*`. CRSR moves the bar (left and right turn the pages), a typed number lights a page, RETURN opens it or goes down a level, `Q` (or ←) goes back up. A page is shown a screenful at a time (RETURN: more, `Q`: back), with its public address at the end. On the top level `S` is the supervisor login and `R` re-reads the index.

# Data source

Two endpoints of this wiki (templates in `themes/ttgwiki2/templates/*.json.twig`, pages in `pages/custom/`):

- `GET /custom/sitemap.json` — every routable page: `route`, `title`, `modified`, `updatedAt`, `tags`, and `private` (true when the page or an ancestor has `access: site.login`). No bodies.
- `GET /custom/page.json?route=/platforms/tic80` — one page with its `markdown` and `html`. A private page needs `token=` matching the wiki's `SUPERVISOR_PASSWORD`, otherwise the answer is `{"error": "forbidden"}`; an unknown route gives `{"error": "not_found"}`.

The page index is loaded once at boot and shared by every session; F8 (`R` on the C64) re-reads it for everyone. If the wiki is down at boot (a stack deploy recreates Grav at the same time), the board still starts and retries the index every 10 seconds. Page bodies are cached for 5 minutes.

The lock is enforced on the wiki's side: the Grav entrypoint puts `SUPERVISOR_PASSWORD` into the site config, and the JSON API refuses private bodies without a matching token. The board sends its `WIKI_BBS_SUPERVISOR_PASSWORD` as that token, so the two must be the same value; the compose file wires both from one variable.

# Under the hood

| Variable | Default | Purpose |
|---|---|---|
| `BBS_PORT` | `2323` | Telnet port inside the container |
| `BBS_MAX_SESSIONS` | `100` | Callers at once (the compose file sets 50) |
| `BBS_IDLE` | `900` | Idle disconnect, in seconds |
| `WIKI_BBS_URL` | `http://localhost:9120` | The wiki's base URL (`http://wiki-grav` on the interstack network) |
| `WIKI_BBS_SUPERVISOR_PASSWORD` | empty | Unlocks the private pages and is sent to the wiki as the API token; empty keeps them locked |

The code follows the house layout: `lib/domain/` (`PageModel`, `WikiRepository`, `WikiService`, `SupervisorService`, and `WikiBBS`, the composition root that reads the environment) and `lib/ui/` (`UI::Modern`, the two-pane browser; `UI::Petscii` with its `Browser`). The full reference is the repository's [README](https://git.teletypegames.org/bbs/wiki-bbs/src/branch/master/README.md).

The service and the container are `wiki-bbs`, in `/srv/stacks/wiki-bbs`, public port 2325. A push to master deploys it from Woodpecker (RuboCop, compose check, `ci-deploy wiki-bbs`); it reaches Grav as `http://wiki-grav`, the wiki container's name on `interstack`. The entrypoint updates rubbs on start. See the [BBS Network](https://wiki.teletypegames.org/services/bbs-network) for the other boards.

'; var prevLevel = 2; for (var i = 0; i < headings.length; i++) { var h = headings\[i\]; var text = h.textContent.trim(); var slug = text.toLowerCase() .replace(/\[áàâä\]/g,'a').replace(/\[éèêë\]/g,'e').replace(/\[íìîï\]/g,'i') .replace(/\[óòôöő\]/g,'o').replace(/\[úùûüű\]/g,'u') .replace(/\[^a-z0-9\]+/g, '-').replace(/^-|-$/g, ''); if (!slug) slug = 'heading-' + i; if (used\[slug\]) { slug += '-' + i; } used\[slug\] = true; h.id = slug; var level = parseInt(h.tagName.charAt(1)); var cls = level > 2 ? ' class="toc-child"' : ''; html += '- [' + text + '](#'%20+%20slug%20+%20')
'; } html += '
'; toc.innerHTML = html; // Scroll-spy: highlight current heading var links = toc.querySelectorAll('a'); var ids = \[\]; for (var j = 0; j < links.length; j++) ids.push(links\[j\].getAttribute('href').slice(1)); function updateActive() { var current = ''; for (var k = 0; k < ids.length; k++) { var el = document.getElementById(ids\[k\]); if (el && el.getBoundingClientRect().top <= 80) current = ids\[k\]; } for (var m = 0; m < links.length; m++) { links\[m\].parentNode.classList.toggle('active', links\[m\].getAttribute('href') === '#' + current); } } window.addEventListener('scroll', updateActive, { passive: true }); updateActive(); })();

---

## Navigation

- Parent: [Services](https://wiki.teletypegames.org/services.md)
- Previous: [WarpEngine BBS](https://wiki.teletypegames.org/services/warp-engine-bbs.md)
- Next: [Wiki Site](https://wiki.teletypegames.org/services/wiki-site.md)
