Skip to content

Commit 3584e01

Browse files
ralyodioclaude
andauthored
Say what tsbb is: a platform with front doors, not only a forum (#19)
The front page, the About page, the docs index and llms.txt each described the board in their own words, and between them they undersold it: an API, a CLI, an MCP server, an installable app, a terminal client, runtime plugins and self-updating installs were either buried in the README or nowhere. The claims now live in apps/server/src/platform.ts and every surface renders the same list. Each entry carries a status: `live` means it is in this repository today, `planned` renders as a badge with no link. Peer to peer is the only planned one, and it is marked as such on the front page, on About, in the docs index and in llms.txt, because a roadmap item written as a feature is the one lie a marketing page tells by accident. New documentation for the front doors that had none: docs/AGENTS.md (what the board publishes for machines, and why agent-ok and human-ok are the same board), docs/PWA.md, docs/UPDATES.md and docs/SKINS.md. They are served at /docs like every other guide and go into llms-full.txt and the sitemap. test/platform.test.ts holds the copy to the code: every live claim must link at something answering 200, a planned one must have no link and must render as planned everywhere, llms.txt must say the same thing as the page, and the grid must carry no inline style attribute. Also here, because they were in the way: - `board.showPlatform` (Appearance, on by default) hides the panel for a board whose readers are not looking to run one. Members never see it. - llms.txt link labels come from each document's own <h1> rather than the first clause of its blurb, which for a blurb with no colon printed the whole sentence as the link text. - The docs page footer line used a style="…" attribute, which the board's own CSP refuses, so it had been rendering unspaced. It is a class now. Claude-Session: https://claude.ai/code/session_01QuUkTrofSjQ15j79mRuy4f Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 6290b16 commit 3584e01

15 files changed

Lines changed: 747 additions & 41 deletions

File tree

‎README.md‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,28 @@ A TypeScript bulletin board. Forums, topics, replies, moderation, private
66
messages, avatars, signatures, search, feeds — and a plugin system that ships
77
with the board rather than being bolted on later.
88

9-
If you have run phpBB, SMF or vBulletin, you already know what this is. The
10-
difference is what it is made of: TypeScript that runs unbuilt, one SQLite file
11-
by default, no client-side JavaScript, and a terminal client.
9+
If you have run phpBB, SMF or vBulletin, you already know what this is. What is
10+
different is everything around it. One board answers as pages, as an installable
11+
app, over a REST API, from a shell, through MCP and in a terminal, all of them
12+
resolving the same permissions. Plugins are directories you drop in, because
13+
there is no build step to stop you. A board fetches its own updates. Agent-ok
14+
and human-ok are the same board, not two products.
15+
16+
| Front door | |
17+
|---|---|
18+
| **Pages** | Server-rendered HTML, no client-side JavaScript, three skins. |
19+
| **App** | An installable PWA: manifest, service worker, offline page. [Docs](docs/PWA.md) |
20+
| **API** | `/api/v1`, permission-checked, with an OpenAPI description. [Docs](docs/API.md) |
21+
| **CLI** | `tsbb`, with `--json` on every command. [Docs](docs/CLI.md) |
22+
| **MCP** | `/api/mcp` over HTTP, `tsbb-mcp` over stdio. [Docs](docs/MCP.md) |
23+
| **Terminal** | `tsbb-tui`, a real client over SSH. [Docs](docs/SKINS.md) |
24+
| **Machines** | llms.txt, skill.md, JSON-LD, sitemap index, OPML. [Docs](docs/AGENTS.md) |
25+
| **Plugins** | A directory in `plugins/`. No build, no registry. [Docs](docs/PLUGINS.md) |
26+
| **Updates** | A board installs new releases itself. [Docs](docs/UPDATES.md) |
27+
28+
**Not yet: peer to peer.** Boards connecting to other boards, and syncing topics
29+
between nodes, is the direction and is not in the code. Everything else on this
30+
page is.
1231

1332
```
1433
pnpm install
@@ -37,6 +56,9 @@ Open <http://localhost:3000>, put in your email address, and click the link.
3756
| **An API** | A permission-checked REST API with an OpenAPI description at `/api/v1/openapi.json`. |
3857
| **A CLI** | `tsbb` reads and posts against any board from a shell, with `--json` on every command. |
3958
| **An MCP server** | Served at `/api/mcp`, and as `tsbb-mcp` over stdio, so an assistant can use the board as a member. |
59+
| **An app** | An installable PWA: a generated manifest, a service worker that is network-first for pages, and an offline page wearing the board's own chrome. |
60+
| **A machine-readable board** | `llms.txt`, `llms-full.txt`, `skill.md`, a sitemap index, `security.txt`, JSON-LD on every page, OPML for the feeds. |
61+
| **Self-updating** | A board checks for a new release a minute after boot and every five minutes, installs it and restarts itself. |
4062

4163
## Design decisions worth knowing before you read the code
4264

@@ -87,6 +109,8 @@ else.
87109
| `classic` | A 2000s bulletin board: boxy, dense, gradient title bars, Verdana. |
88110
| `terminal` | Neutral surfaces, hairline rules, monospace chrome, window furniture on section headers. |
89111

112+
Full guide: **[docs/SKINS.md](docs/SKINS.md)**.
113+
90114
`classic` and `terminal` are **layers on top of** the modern sheet rather than
91115
replacements, so a component's structure is defined in exactly one place and a
92116
skin only argues about how it looks. Two full stylesheets drift apart within a
@@ -273,6 +297,8 @@ The worker runs inside the server by default, so email works from one command.
273297

274298
## Updates
275299

300+
Full guide: **[docs/UPDATES.md](docs/UPDATES.md)**.
301+
276302
A board keeps itself current. A minute after it starts, and every five minutes
277303
after that, it asks GitHub for the newest release; when there is one it fetches
278304
the tag, runs `pnpm install`, and restarts itself. The whole thing is the three

‎apps/server/src/platform.ts‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/**
2+
* What tsbb is, said once.
3+
*
4+
* The front page, the About page, the docs index and llms.txt all describe the
5+
* platform, and four hand-written copies of the same list drift apart within a
6+
* release: one gains a feature, another keeps a claim that stopped being true.
7+
* So the list lives here and every page renders it.
8+
*
9+
* Every entry marked `live` is backed by code in this repository today. An
10+
* entry marked `planned` is on the roadmap and is rendered as such, never as
11+
* a feature. Move it to `live` in the pull request that makes it true, not
12+
* before.
13+
*/
14+
import { html } from 'hono/html';
15+
import { Badge } from '@tsbb/ui';
16+
17+
export type FrontDoorStatus = 'live' | 'planned';
18+
19+
export interface FrontDoor {
20+
/** A stable key, used for a class name and a JSON-LD feature id. */
21+
key: string;
22+
title: string;
23+
/** One or two sentences. Plain text: it is rendered escaped everywhere. */
24+
summary: string;
25+
/** Where to read more. Absent for a planned entry: there is nothing to read yet. */
26+
href?: string;
27+
status: FrontDoorStatus;
28+
}
29+
30+
export const PLATFORM_NAME = 'tsbb';
31+
export const PLATFORM_TAGLINE = 'A TypeScript bulletin board';
32+
33+
/** The one-line positioning, used as a heading wherever the grid appears. */
34+
export const PLATFORM_CLAIM = 'The new bulletin board platform';
35+
36+
/**
37+
* The paragraph under that heading. Everything in it is true of the code; the
38+
* only forward-looking clause is the last one, and it says so.
39+
*/
40+
export const PLATFORM_LEAD =
41+
'One board, reachable every way people and programs read the web: as pages, as an installable app, ' +
42+
'over a REST API, from a shell, through MCP, and from a terminal. Runtime plugins, no build step, ' +
43+
'self-updating installs. Boards that connect to other boards are next.';
44+
45+
export const FRONT_DOORS: readonly FrontDoor[] = [
46+
{
47+
key: 'api',
48+
title: 'REST API',
49+
summary:
50+
'Everything the pages show, at /api/v1, resolved through the same permission checks, and described by an OpenAPI file.',
51+
href: '/docs/api',
52+
status: 'live',
53+
},
54+
{
55+
key: 'cli',
56+
title: 'CLI',
57+
summary:
58+
'The tsbb command runs a board, and reads and posts against any board from a shell, with --json on every command.',
59+
href: '/docs/cli',
60+
status: 'live',
61+
},
62+
{
63+
key: 'mcp',
64+
title: 'MCP server',
65+
summary:
66+
'Served at /api/mcp over streamable HTTP, and as tsbb-mcp over stdio. An assistant reads, searches and posts as a member.',
67+
href: '/docs/mcp',
68+
status: 'live',
69+
},
70+
{
71+
key: 'agents',
72+
title: 'Agent-ok, human-ok',
73+
summary:
74+
'A browser, a script and a model all get the same content under the same permissions. llms.txt, skill.md and an explicit welcome in robots.txt.',
75+
href: '/docs/agents',
76+
status: 'live',
77+
},
78+
{
79+
key: 'pwa',
80+
title: 'Installable PWA',
81+
summary:
82+
'A manifest, a service worker and an offline page. Installs on a phone or a desktop and keeps what you have read when the connection goes.',
83+
href: '/docs/pwa',
84+
status: 'live',
85+
},
86+
{
87+
key: 'plugins',
88+
title: 'Plugins, no build step',
89+
summary:
90+
'A plugin is a directory. Filters, actions, slots, settings and routes, loaded at boot. Drop it in and restart.',
91+
href: '/docs/plugins',
92+
status: 'live',
93+
},
94+
{
95+
key: 'updates',
96+
title: 'Self-updating',
97+
summary:
98+
'A board checks GitHub releases a minute after boot and every five minutes, installs the new one and restarts itself.',
99+
href: '/docs/updates',
100+
status: 'live',
101+
},
102+
{
103+
key: 'skins',
104+
title: 'Terminal skin, terminal client',
105+
summary:
106+
'Three skins over one set of markup, one of them a terminal look. And tsbb-tui, for reading and posting over SSH.',
107+
href: '/docs/skins',
108+
status: 'live',
109+
},
110+
{
111+
key: 'feeds',
112+
title: 'Feeds both ways',
113+
summary:
114+
'RSS for the board, every forum, thread, member and search. And a forum can be filled from any RSS or Atom feed.',
115+
href: '/feeds',
116+
status: 'live',
117+
},
118+
{
119+
key: 'p2p',
120+
title: 'Peer to peer',
121+
summary:
122+
'Boards that connect to other boards and sync topics between nodes. On the roadmap, not in the code yet.',
123+
status: 'planned',
124+
},
125+
];
126+
127+
export const LIVE_FRONT_DOORS: readonly FrontDoor[] = FRONT_DOORS.filter((door) => door.status === 'live');
128+
129+
/**
130+
* The grid, as markup.
131+
*
132+
* Rendered on the front page, the About page and the docs index. A planned
133+
* entry renders with a "planned" badge and no link, which is the whole reason
134+
* status is data rather than prose: a page cannot accidentally present it as
135+
* something that works today.
136+
*/
137+
export function PlatformGrid(doors: readonly FrontDoor[] = FRONT_DOORS) {
138+
return html`<div class="platform-grid">
139+
${doors.map(
140+
(door) => html`<div class="platform-item platform-${door.key}">
141+
<div class="platform-item-head">
142+
${door.href
143+
? html`<a class="platform-item-title" href="${door.href}">${door.title}</a>`
144+
: html`<span class="platform-item-title">${door.title}</span>`}
145+
${door.status === 'planned' ? Badge('Planned', 'outline') : ''}
146+
</div>
147+
<p class="platform-item-summary">${door.summary}</p>
148+
</div>`,
149+
)}
150+
</div>`;
151+
}

‎apps/server/src/routes/admin.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export function adminRoutes(services: Services) {
215215
'board.logoUrl',
216216
'board.logoHref',
217217
'board.faviconUrl',
218+
'board.showPlatform',
218219
],
219220
},
220221
{ title: 'Registration', keys: ['registration.mode', 'registration.minUsernameLength', 'registration.maxUsernameLength'] },
@@ -243,6 +244,8 @@ export function adminRoutes(services: Services) {
243244
'board.logoHref':
244245
'Where the header logo points. / is this board. An absolute URL is for a board that is one room in a larger site — the nav still leads back to the front page, so nobody is stranded.',
245246
'board.faviconUrl': 'A URL to a browser-tab icon. Replaces the bundled tsbb icons.',
247+
'board.showPlatform':
248+
'A panel at the foot of the front page telling a visitor what the software under this board can do: the API, the CLI, MCP, the app, plugins. Guests only, never members. Turn it off for a board whose readers are not looking for one of their own.',
246249
'signatures.minPosts':
247250
'How many posts before a signature is shown. A new account with a link-filled signature is the shape of every piece of forum spam, so this is 10 by default.',
248251
'posts.floodSeconds': 'Seconds between posts by the same account. 0 turns flood control off.',

‎apps/server/src/routes/board.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
} from '@tsbb/ui';
4646
import type { Post, User, Viewer } from '@tsbb/plugin-api';
4747
import { render, slot, type AppEnv, type Services } from '../context.ts';
48+
import { PLATFORM_CLAIM, PLATFORM_LEAD, PlatformGrid } from '../platform.ts';
4849

4950
export function boardRoutes(services: Services) {
5051
const app = new Hono<AppEnv>();
@@ -91,6 +92,7 @@ export function boardRoutes(services: Services) {
9192
);
9293
})()}
9394
${boardStatsPanel(stats)}
95+
${platformPanel(settings, viewer)}
9496
${trusted(below)}`;
9597

9698
return render(c, services, {
@@ -313,6 +315,33 @@ function boardHero(settings: Settings, viewer: Viewer) {
313315
</section>`;
314316
}
315317

318+
/**
319+
* What the software under this board is, for somebody who has never seen it.
320+
*
321+
* Guests only, and last on the page: a member came for the forum, and a visitor
322+
* reads the forums first and the sales pitch second. The grid itself is data in
323+
* platform.ts, so a claim made here is a claim made everywhere, and a feature
324+
* that does not exist yet renders as planned rather than as a feature.
325+
*/
326+
function platformPanel(settings: Settings, viewer: Viewer) {
327+
if (viewer.user || settings['board.showPlatform'] === false) return '';
328+
329+
return Card(html`
330+
${CardHeader(PLATFORM_CLAIM, { description: 'This board runs on tsbb. So can yours.' })}
331+
${CardContent(html`
332+
<p class="platform-lead">${PLATFORM_LEAD}</p>
333+
${PlatformGrid()}
334+
<div class="row platform-actions">
335+
${LinkButton('Read the docs', '/docs', { size: 'sm' })}
336+
${LinkButton('About this board', '/about', { size: 'sm', variant: 'outline' })}
337+
<!-- Off-site, so it carries rel="noopener" of its own rather than going
338+
through LinkButton, which has no rel. -->
339+
<a href="https://github.com/profullstack/tsbb" class="btn btn-ghost btn-sm" rel="noopener">Source on GitHub</a>
340+
</div>
341+
`)}
342+
`);
343+
}
344+
316345
function boardStatsPanel(stats: BoardStats) {
317346
return Card(html`
318347
${CardHeader('Board statistics')}

‎apps/server/src/routes/discovery.ts‎

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { html } from 'hono/html';
44
import { forumTree, guestViewer, visibleForumIds, type Settings } from '@tsbb/core';
55
import { escapeHtml } from '@tsbb/markup';
66
import { all } from '@tsbb/db';
7-
import { Card, CardContent, LinkButton } from '@tsbb/ui';
7+
import { Card, CardContent, CardHeader, LinkButton } from '@tsbb/ui';
88
import { render, type AppEnv, type Services } from '../context.ts';
9-
import { DOCS, docMarkdown } from './docs.ts';
9+
import { DOCS, docMarkdown, docTitle } from './docs.ts';
10+
import { FRONT_DOORS, LIVE_FRONT_DOORS, PLATFORM_CLAIM, PLATFORM_LEAD, PlatformGrid } from '../platform.ts';
1011

1112
/**
1213
* The files a machine reads before it reads the board.
@@ -255,7 +256,21 @@ export function discoveryRoutes(services: Services) {
255256
`${name} is a bulletin board: a tree of forums, each holding topics, each topic a thread of posts. ` +
256257
'Every public page is complete server-rendered HTML with no client-side script, and every list ' +
257258
'on the board is also an RSS feed. The same content is reachable through a REST API, a command ' +
258-
'line client and an MCP server, all of which apply the permissions the pages do.',
259+
'line client, an MCP server, an installable app and a terminal client, all of which apply the ' +
260+
'permissions the pages do.',
261+
'',
262+
`It runs on tsbb, ${PLATFORM_CLAIM.toLowerCase()}. ${PLATFORM_LEAD}`,
263+
'',
264+
'## What this board can do',
265+
'',
266+
...LIVE_FRONT_DOORS.map((door) =>
267+
door.href && door.href.startsWith('/')
268+
? `- [${door.title}](${absolute(door.href)}): ${door.summary}`
269+
: `- ${door.title}: ${door.summary}`,
270+
),
271+
...FRONT_DOORS.filter((door) => door.status === 'planned').map(
272+
(door) => `- ${door.title} (planned, not built yet): ${door.summary}`,
273+
),
259274
'',
260275
'## Read the board',
261276
'',
@@ -274,7 +289,7 @@ export function discoveryRoutes(services: Services) {
274289
'## Use the board from a program',
275290
'',
276291
link('Documentation', '/docs', 'The index of every guide below.'),
277-
...DOCS.map((doc) => link(doc.blurb.split(':')[0]?.replace(/`/g, '') ?? doc.slug, `/docs/${doc.slug}`, doc.blurb.replace(/`/g, ''))),
292+
...DOCS.map((doc) => link(docTitle(doc), `/docs/${doc.slug}`, doc.blurb.replace(/`/g, ''))),
278293
link('OpenAPI description', '/api/v1/openapi.json', 'The REST API, machine-readable.'),
279294
link('MCP endpoint', '/api/mcp', 'Streamable HTTP MCP server; a bearer token makes it act as a member.'),
280295
link('Agent skill', '/skill.md', 'What an agent can do here and how to authenticate.'),
@@ -340,6 +355,10 @@ export function discoveryRoutes(services: Services) {
340355
'',
341356
`${name} is a forum. Use it to read topics, search posts, and — as a signed-in member — start topics and reply.`,
342357
'',
358+
'It runs on tsbb, which answers the same content four ways: pages, a REST API, a CLI and MCP.',
359+
'Whichever one you use, the permissions are the ones the pages apply. Nothing here is an',
360+
'agent-only view of the board, and nothing is withheld from a browser that a token can see.',
361+
'',
343362
'## Connect',
344363
'',
345364
`- MCP over streamable HTTP: \`${absolute('/api/mcp')}\``,
@@ -418,8 +437,15 @@ export function discoveryRoutes(services: Services) {
418437
<li>A <a href="/docs/api">REST API</a>, described by an <a href="/api/v1/openapi.json">OpenAPI file</a>.</li>
419438
<li>The <a href="/docs/cli"><code>tsbb</code> command line client</a>, with <code>--json</code> on every command.</li>
420439
<li>An <a href="/docs/mcp">MCP server</a>, so an AI assistant can read and post as a member.</li>
440+
<li>An <a href="/docs/pwa">app you can install</a>, and <code>tsbb-tui</code> in a terminal over SSH.</li>
421441
</ul>
422-
<p>They all apply exactly the permissions the pages do.</p>
442+
<p>
443+
They all apply exactly the permissions the pages do, and none of them is a second-class
444+
copy of the site: see <a href="/docs/agents">what this board publishes for machines</a>.
445+
</p>
446+
447+
<h2>What is it built on?</h2>
448+
<p>${PLATFORM_LEAD}</p>
423449
424450
<h2>Who runs it?</h2>
425451
<p>
@@ -430,6 +456,10 @@ export function discoveryRoutes(services: Services) {
430456
</p>
431457
</div>`),
432458
)}
459+
${Card(html`
460+
${CardHeader(PLATFORM_CLAIM, { description: 'tsbb, the software this board runs.' })}
461+
${CardContent(PlatformGrid())}
462+
`)}
433463
<div class="row about-actions">
434464
${!viewer.user && mode !== 'closed' ? LinkButton('Join the board', '/signup', { size: 'sm' }) : ''}
435465
${LinkButton('Read the docs', '/docs', { size: 'sm', variant: 'outline' })}

0 commit comments

Comments
 (0)