npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fedipod-bb

v0.4.4

Published

FediPod-BB: a forum website whose record lives on a Solid pod.

Readme

FediPod-BB

A forum whose record lives on a Solid pod, as ActivityPub documents in the shapes the Fediverse's boards use: each category is a Group actor (FEP-1b12), each topic is a context collection (FEP-7888), each post is a Page (with a title, as Lemmy's are) or a Note that names its topic and its category.

This package holds the pod layout, the writers for it, the host, and the website: where every document lives, how a topic's pages are written and sealed, how a member's post is cached for readers, the process that runs the forum from a moderator's machine, and the page that shows the forum. A moderator's console is part of that page: the queue of reports, held posts and asks, and the settings. The queue shows what needs a moderator: a report, a held post, a join request, and a request of a moderator's that did not take.

npm test

Hosting a forum

A forum can be made at a FediPod Gateway's sign-up page (fedipod.net's, for one) with no machine of your own: choose "A forum", make a pod for it at a provider, name it and its categories, and the Gateway writes it on the pod and runs it from then on. The commands below are for running a forum from a machine of your own instead.

npm install -g fedipod-bb
fedipod-bb credential --home DIR --email [email protected] --pod https://forum.example/
fedipod-bb init --home DIR --handle forum --name "The Forum" \
    --category gardening:Gardening --category compost:Compost --moderator <actor id>
fedipod-bb start --home DIR

credential mints the forum's pod credential at the pod's server, with the account's password asked at the terminal, and saves it as DIR/credential.json; nothing is written to the pod.

init writes the forum's config and containers. start publishes every actor the first time, then drains the forum's one inbox, hands each activity to the category it names, places carried posts in topics, and carries them to the category's followers. Several moderators run start on their own machines against the same pod: one hosts, the others watch, and when the host stops another takes over within fifteen minutes. A public ap/heartbeat says when the forum was last hosted. status prints what the pod's state says without hosting.

Every category is a FediPod group: joining is a Follow, a member's public post addressed to the category is carried (FEP-1b12) and then placed in its topic — the one its context names, the one its reply chain leads to, or a new one with the post as its opening (FEP-7888). A post from a non-member is not carried and opens nothing.

Layout

Under one root, fedipod-bb/, on the forum's pod:

| path | what | |---|---| | ap/actor | the forum's own actor, an Application | | ap/inbox/ | the one inbox every category names as its own | | ap/categories | the categories, an OrderedCollection of their actor ids | | ap/administrators | the moderators' actor ids | | ap-state/ | owner-only: the forum's config, keys and lease | | c/<slug>/ | one FediPod group root per category, unchanged from a group's | | c/<slug>/ap/topics | the category's topics, newest first, paged | | c/<slug>/ap/topic/<tid> | a topic: the context collection, with pages -1, -2, … oldest first | | c/<slug>/ap/cache/<key> | a readable copy of a member's post, for the website | | c/<slug>/ap-state/topics.json, topics/<tid>.json | owner-only: the topic record |

A category's ap/actor, ap/outbox, ap/followers, ap/moderators and ap/featured are what a FediPod group publishes today; the forum adds the topic documents beside them. Posts are named in a topic by their authors' own ids; the cached copy is for readers in a browser and is never listed in a collection.

The website

site/ is a static page. Staged by scripts/stage-site.mjs under /bb/ on the fedipod.net site, it opens a forum attached there at /bb/?forum=<handle>, reading everything through that site's own /u/<handle>/ addresses; /bb/?pod=https://…/fedipod-bb/ reads a forum from its pod directly. Where the site has bb.<domain> as a domain alias, the same page answers there for every path, with the forum named by the first path segment: https://bb.fedipod.net/<handle>/. It still reads through <domain>, where the handles live (the hosts are BB_HOSTS in the staging script). Anyone reads. A topic whose newest post arrived since the reader last opened it is marked New on the index. The mark is the reader's own: one entry per topic in their browser's storage, written nowhere else and sent nowhere - a public forum has no place to keep who read what, and no business keeping it. Arriving at a forum for the first time reads everything before that moment, so a new reader does not meet a page of New.

The page is built to WCAG 2.2 AA: a skip link, named landmarks, a label on every control, an aria-label on each post's buttons naming whose post they act on, aria-current on the category in view, scope on the index's headers, a polite live region for what the page has just done, and a reply box that takes the keyboard when it opens and returns it when it closes. The front page is an index of the forum's newest posts, across every category: topic, category, author, date and the topic's reply count, one line each. The forum publishes that index itself (ap/latest, the copies it already holds, newest first, capped at fifty), so the page makes one request for the list rather than walking every category. The category chips filter it; a topic's name opens the thread at that post. Pinned topics sort first - a category's featured collection for a pin within it, the forum actor's own for one that holds everywhere.

A thread is a tree: a reply sits under the post it answers, six deep. Posts are written in Markdown (site/markdown.mjs, no dependency, escaped before it is marked up) and carry their source alongside, so an edit reopens what was typed. A topic is named by the activity that opens it; a post has no title.

Each post offers Reply, Share and Report, and its author Edit and Delete; a report is queued for the moderators. Under a topic's title a moderator gets rename, pin, pin site-wide and delete. Every moderator request is published at the moderator's own pod and fetched back from there before the forum acts on it (FEP-fe34), which is what makes a button on a public page safe. A post can be voted for: a Like to the category, its Undo to take it back, counted per person and published with the post as AS2 likes. The index sorts by newest or by votes, and searches what it is holding - topic names, authors and the words of the posts. A byline opens that person's posts in this forum.

A moderator's queue lives at <pod>/fedipod-bb/mod/queue.json, with a record of what was done beside it in log.json. Neither is public: the container's access rule names the moderators' WebIDs, and the page reads it with the moderator's own login rather than through the Gateway. From it a moderator lets a held post through (Accept), turns it away (Reject) or bans its author (Block) - each published at their own pod and checked there, like every other ask.

A moderator gets a Settings page: rename the forum, rename a category, manage moderators and members, and make a new category. Each row carries Open and Private. Every change is an ordinary activity - a Create of a Group, an Update, an Add or a Remove - published at the moderator's own pod and fetched back before it is applied.

Open or private belongs to the category, and only the forum changes it. Joining does not, being admitted does not, and being removed does not. On the wire it is manuallyApprovesFollowers on the Group.

A private category means all of this, and no more:

  • Its trees are readable by named WebIDs only. The actor stays public, since a server that cannot read the actor cannot deliver to the category.
  • It publishes c/<slug>/ap/members, the WebIDs that may read it, readable only by those same WebIDs. The forum's own WebID is one of them, because the forum fetches every post back from its author's pod.
  • Every join waits for a moderator, and admitting somebody requires a WebID. A follower without one is let go with a Reject: carrying posts to somebody refused the pages would not be private at all.
  • A member's post is addressed to the category's followers collection rather than to the public, and is written into a container of its own on the member's pod whose access rule names the same reader list. One container per category, rewritten on each post, so one rule covers that member's whole history there.

What it does NOT mean, and the page says so in the box before anyone writes: an ejected reader keeps a given author's older posts until that author next writes; somebody admitted today reads the whole history; posts written while the category was open stay public; pod operators hold the plaintext; and any admitted member can copy anything.

The command-line equivalents are --members-only <slug> and --member <slug>:<webid>. A draft FEP for the layout and this model is in fep-draft.md. To reply, a reader signs in once with a Mastodon account: the page registers itself on their server, sends them to approve it, and keeps the token in their browser; a reply is posted from their account with the category mentioned, and answers the thread's last post as their server resolves it. The reply shows in the thread once the forum's host has placed it; until then the reader sees their own copy marked as waiting. A reader with a pod account is pointed at FediPod and the category's address; a reader with nothing is pointed at sign-up.

Attaching to a Gateway

fedipod-bb attach --home DIR --front https://fedipod.net

Takes @<handle>@fedipod.net for the forum and one address per category, each a row of its own at the Gateway pointing at that category's tree on the pod and at the forum's one inbox. Deliveries arrive verified at the front; the next start republishes every actor under its front address.

A post is voted up or down, the way Lemmy federates a vote: a Like for up, a Dislike for down, and the Undo of whichever was cast to take it back. One person has one vote, so voting the other way is a changed mind rather than a second vote. The up count is AS2's likes on the post's copy. ActivityStreams has no property for a down count and none is invented: it is an ordinary Collection published beside the copy, at that copy's address with -dislikes after it, and taken down again when it reaches nought. The copy also carries the count itself, as dislikes shaped like likes, which is the forum's own word; the page reads it there rather than asking for a document beside every post it shows.