A CLI tool that implements 4chan-style thread auto-archiving and purging for 5chan boards.
- After each board update, determine thread positions in active sort
- Filter out pinned threads (they're exempt)
- Count non-pinned threads; any beyond position
per_page × pages→ archive viacreateCommentModeration({ commentModeration: { archived: true } }) - Archived threads are read-only (pkc-js already enforces this)
The board's bump order comes from @pkcprotocol/active-page-sort, which the default preset installs as the board's active post sort (see Page sorts):
- A reply whose content has a line equal to
sagedoes not bump its thread - Past
bump_limitreplies a thread stays open and keeps taking replies, but none of them bump it. It sinks, and is archived when it falls off the last page (Feature 1), as on 4chan. Asagereply still counts toward the limit - The board manager does not archive or lock threads at the bump limit. Instead it writes the board's
bumpLimitinto the sort'sbumpLimitoption:5chan board add --apply-defaultswrites the board's own value, and each time a board manager starts it puts the configured value back if they differ (the board manager config is the source of truth). A change throughboard edit --bump-limit,defaults set --bump-limitor a config file restarts the board manager, which then updates the sort. After changing the limit it holds capacity archiving until the board publishes a record generated after the edit (a few publish cycles; a quiet board republishes at least every 15 minutes), because until then the pages are still ordered by the old limit - A board whose
settings.pagesdoes not name@pkcprotocol/active-page-sort(by registryname;path:entries are left alone) has no bump limit; the board manager logs thatbumpLimithas no effect there - Threads archived at the bump limit by earlier versions stay archived
- Track when threads were archived
- After
archive_purge_secondshas elapsed since archiving → purge viacreateCommentModeration({ commentModeration: { purged: true } }) - Purging is time-based rather than activity-based, so it also runs on a periodic sweep (
PURGE_SWEEP_INTERVAL_SECONDS, default 60s) instead of only when acommunity.updateevent arrives. Without the sweep, a board that went quiet after archiving would keep expired threads in the archive until unrelated activity woke the manager.
A board whose community record sets features.noArchive: true has no archive, like 4chan's /b/ and /trash/:
- Threads beyond position
per_page × pagesare purged right away viacreateCommentModeration({ commentModeration: { purged: true } })instead of archived - Threads past
bump_limitare not locked: like on any other board they sink in the bump order and are purged once they fall past capacity - Threads archived earlier, by the board manager or by hand, are purged once they are past capacity, since clients no longer list an archive for the board. Ones the board manager archived also still expire through
archive_purge_seconds - The flag is read from the community on every update, so it takes effect without restarting the board manager. pkc-js rejects archive moderations on such a board, and 5chan hides its archive
Turn it on with bitsocial-cli (bitsocial community edit <address> --features.noArchive), or by uncommenting noArchive in the preset before running 5chan board add.
- On each update, scan comments and replies for
deleted === true - Purge via
createCommentModeration({ commentModeration: { purged: true } }) - Duplicate purge moderations (if the board hasn't processed prior purge yet) are harmless no-ops
pkc-js 0.0.132 adds comment.gallery, a list of extra media links on a post or reply next to link. A board only accepts galleries when its community record sets features.maxPostGalleryItems and features.maxReplyGalleryItems; absent or 0 rejects any publication that carries a gallery.
The preset sets both to 0: a post or reply may have a link but no gallery, like 4chan's one file per post. The count is on top of link, because requirePostLink and requirePostLinkIsMedia only look at link, so a board that allows 4 files per post sets maxPostGalleryItems: 3. noImages, noVideos, noAudio and their reply versions apply to every gallery item as well.
Raise the limits in the preset before running 5chan board add, or on an existing board with bitsocial-cli (bitsocial community edit <address> --features.maxPostGalleryItems 3 --features.maxReplyGalleryItems 3). bitsocial-cli 0.19.119 or newer (pkc-js 0.0.132) enforces them; 0.19.116 to 0.19.118 stores the fields without checking galleries. The 5chan frontend does not render or publish galleries yet, so until it does, raising the limits only affects other clients.
Config and state are stored per-board under ~/.config/5chan/, managed via 5chan board add/edit/remove or manual editing:
~/.config/5chan/
├── global.json # shared settings (rpcUrl, defaults)
└── boards/
├── random.bso/
│ ├── config.json # { "address": "random.bso" }
│ ├── state.json # auto-created (signers, archivedThreads)
│ └── state.json.lock # transient lock file
├── tech.bso/
│ └── config.json # { "address": "tech.bso", "bumpLimit": 500 }
├── flash.bso/
│ └── config.json # { "address": "flash.bso", "perPage": 30, "pages": 1 }
└── custom.bso/
└── config.json # { "address": "custom.bso", "moderationReasons": { ... } }
global.json (optional):
{
"rpcUrl": "ws://localhost:9138",
"defaults": {
"perPage": 15,
"pages": 10,
"bumpLimit": 300,
"archivePurgeSeconds": 172800,
"moderationReasons": {
"archiveCapacity": "5chan board manager: thread archived — exceeded board capacity",
"purgeArchived": "5chan board manager: thread purged — archive retention expired",
"purgeDeleted": "5chan board manager: content purged — author-deleted",
"purgeCapacity": "5chan board manager: thread purged — exceeded board capacity"
}
}
}Minimal config: a single directory boards/my-board.bso/config.json containing { "address": "my-board.bso" }
All fields except each board's address are optional:
rpcUrl— falls back toPKC_RPC_WS_URLenv var, thenws://localhost:9138defaults— applied to all boards unless overridden per-board- Per-board fields (
perPage,pages,bumpLimit,archivePurgeSeconds,moderationReasons) overridedefaults moderationReasons— optional object witharchiveCapacity,purgeArchived,purgeDeleted,purgeCapacitystring fields (purgeCapacityapplies only to boards without an archive).archiveBumpLimitandlockBumpLimitare deprecated: still accepted so older configs load, but ignored (a warning is logged), since threads at the bump limit are no longer archived or locked. Per-board values override defaults per-field (not the whole object). These reason strings are passed tocreateCommentModeration()so PKC clients can display why a thread was archived or purged.- Board directory names must match the address field in
config.json
Docker is the only supported install path. The published image
(ghcr.io/bitsocialnet/5chan-board-manager:latest) is built and tested with the
bitsocial-cli version the default preset assumes (pinned in the compose files;
the board manager needs bitsocial-cli 0.19.116 or newer: its pkc-js 0.0.129 changed
the shape of the communities list the RPC sends, and older servers no longer connect),
including the @bitsocial/spam-blocker-challenge and @pkcprotocol/active-page-sort
packages that the preset references. Running 5chan
outside Docker — or against a bitsocial-cli you manage yourself — is possible
but unsupported; you will have to keep the challenge and auth-key wiring in
sync by hand (see Standalone below).
If you don't already have bitsocial-cli running, use the full stack compose file which boots both bitsocial-cli (PKC RPC server) and 5chan together.:
wget -O docker-compose.yml https://raw.githubusercontent.com/bitsocialnet/5chan-board-manager/master/docker-compose.example.yml
docker compose up -d
# Install the spam-blocker challenge referenced by the default preset
# (skip this only if you also remove spam-blocker entries from your preset
# or always use --skip-apply-defaults):
docker compose exec bitsocial bitsocial challenge install @bitsocial/spam-blocker-challenge
# Install the bump-order page sort referenced by the default preset
# (skip this only if you also remove the settings.pages posts entry from your
# preset or always use --skip-apply-defaults):
docker compose exec bitsocial bitsocial page-sort install @pkcprotocol/active-page-sort@0.0.7
# Optional: install this before enabling the wordfilter preset example
docker compose exec bitsocial bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0
# Now add boards via 5chan board add (see Config Directory Layout above)See docker-compose.example.yml for the full configuration.
Use this flow to create a new board with bitsocial-cli and immediately add it to 5chan.
Note: The compose file ships with a pre-configured RPC auth key so both containers can connect out of the box. For production, replace it with your own random string:
sed -i "s/TFCh0joRU60KwlfVaprP2uenw7NCdAwsBCF5UDoVg/$(openssl rand -base64 32 | tr -d '/+=')/g" docker-compose.yml
Note: The container starts gracefully even with no boards configured — it waits for boards to be added and picks them up automatically via config hot-reload.
wget -O docker-compose.yml https://raw.githubusercontent.com/bitsocialnet/5chan-board-manager/master/docker-compose.example.yml
docker compose up -d
# Install the spam-blocker challenge referenced by the default preset
docker compose exec bitsocial bitsocial challenge install @bitsocial/spam-blocker-challenge
# Install the bump-order page sort referenced by the default preset
docker compose exec bitsocial bitsocial page-sort install @pkcprotocol/active-page-sort@0.0.7
# Optional: install this before enabling the wordfilter preset example
docker compose exec bitsocial bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0
# Create a community (copy the created address from output)
docker compose exec bitsocial bitsocial community create \
--title "My Board title" \
--description "My Board description"
# Add the created community to 5chan board manager
docker compose exec -it 5chan 5chan board add <community-address>
# Verify it was added
docker compose exec 5chan 5chan board list
# You can load the board now by its address in 5chan or any other web uiIf you already have bitsocial-cli running separately (on the host, in another compose stack, etc.), use the standalone compose file which only runs 5chan:
cp docker-compose.standalone.example.yml docker-compose.yml
# Edit PKC_RPC_WS_URL in docker-compose.yml — replace YOUR-AUTH-KEY with
# your bitsocial-cli auth key (find it with: docker logs <bitsocial-container> 2>&1 | grep "secret auth key")
docker compose up -dSet PKC_RPC_WS_URL to the address of your existing instance, including the auth key as a path segment:
- bitsocial-cli on the host (no container): Use
ws://host.docker.internal:9138/YOUR-AUTH-KEY. The example compose file includesextra_hosts: ["host.docker.internal:host-gateway"]so this works on Linux, macOS, and Windows. - bitsocial-cli in another Docker container/network: Use the container or service name, e.g.
ws://bitsocial:9138/YOUR-AUTH-KEY, and make sure both containers share the same Docker network.
You must install the spam-blocker challenge on your bitsocial-cli instance, because the default preset (src/presets/community-defaults.jsonc) references it and 5chan board add --apply-defaults will be rejected by the PKC RPC server if the challenge is missing:
bitsocial challenge install @bitsocial/spam-blocker-challengeIf you do not want this dependency, remove both @bitsocial/spam-blocker-challenge entries from your preset (they are marked optional in the file) or use --skip-apply-defaults.
You must also install the bump-order page sort (bitsocial-cli 0.19.116 or newer, the board manager's minimum). The default preset names it in settings.pages, and the RPC server rejects 5chan board add --apply-defaults with ERR_RPC_CLIENT_PAGE_SORT_NAME_NOT_AVAILABLE_ON_SERVER when it is missing:
bitsocial page-sort install @pkcprotocol/active-page-sort@0.0.7Without it, remove the @pkcprotocol/active-page-sort entry from settings.pages.posts in your preset or use --skip-apply-defaults. The board then has no bump limit and no sage (see Feature 2).
The bundled preset also contains a disabled wordfilter example. If you enable it, install the challenge on the same bitsocial-cli instance first:
bitsocial challenge install @bitsocial/wordfilter-challenge@0.3.0See docker-compose.standalone.example.yml for the configuration.
Use docker compose exec to run additional 5chan CLI commands inside the running container:
# Add a board
docker compose exec 5chan 5chan board add random.bso
# List configured boards
docker compose exec 5chan 5chan board list
# Edit a board's config
docker compose exec 5chan 5chan board edit random.bso --bump-limit 500
# Open a board's config in $EDITOR for interactive editing
docker compose exec 5chan 5chan board edit random.bso -i
# Reset a board field to global default
docker compose exec 5chan 5chan board edit random.bso --reset per-page
# Reset moderation reasons to defaults
docker compose exec 5chan 5chan board edit random.bso --reset moderation-reasons
# Set global defaults for all boards
docker compose exec 5chan 5chan defaults set --per-page 20 --bump-limit 500
# Open global defaults in $EDITOR for interactive editing
docker compose exec 5chan 5chan defaults set -i
# Remove a board
docker compose exec 5chan 5chan board remove random.bsoThe container auto-reloads when files in the config directory change, so boards added/edited via 5chan board add/edit take effect immediately without restarting.
docker build -t 5chan-board-manager .
docker run -d -v /path/to/data:/data 5chan-board-manager| Container path | Description |
|---|---|
/data/5chan/global.json |
Global config (optional — rpcUrl, defaults) |
/data/5chan/boards/<address>/config.json |
Per-board config (created by 5chan board add) |
/data/5chan/boards/<address>/state.json |
Per-board state (auto-created: signers, archived threads) |
If you are not using the Docker quick usage flow above, create the board with bitsocial community create first, then add it to 5chan with 5chan board add.
Media rendering: Keep
settings.fetchThumbnailUrlsenabled for 5chan boards. Bitsocial publications store media links, not uploaded media files, so clients need the thumbnail metadata fetched by the community node to know image and video dimensions before rendering. Without it, clients must fall back to generic containers that can size media poorly or cause layout shifts.The bundled preset enables this setting for new boards. By default, the community node fetches media links directly, which exposes the node's IP address to those hosts. Operators who need to hide the node's IP should also configure
settings.fetchThumbnailUrlsProxyUrl.
Run 5chan board add --help for full details on preset defaults flags (--apply-defaults, --skip-apply-defaults, --interactive-apply-defaults). In interactive terminals, defaults are shown with an [A]ccept / [M]odify / [S]kip prompt; choosing Modify opens the preset in $EDITOR as an annotated JSONC file with // comments explaining each field.
Preset JSONC is validated with Zod. Both plain JSON and JSONC (with // comments) are accepted as preset files.
boardSettings must follow pkc-js CommunityEditOptions:
https://github.com/pkcprotocol/pkc-js?tab=readme-ov-file#communityeditcommunityeditoptions
Moderator bypass: The bundled preset exempts moderators from its challenges with
{ "roles": ["moderator", "admin", "owner"] }. Nodes on pkc-js 0.0.101 or later reject the olderexclude.rolefield (ERR_CHALLENGE_EXCLUDE_ROLE_FIELD_RENAMED), so custom presets must userolesas well. Nodes on older pkc-js acceptrolesbut ignore it, so their moderators get the preset's challenges like other users.
Bundled preset JSONC defaults:
src/presets/community-defaults.jsonc
boardSettings is merged into community.edit() with "missing only" semantics (only absent values are applied). Because pkc-js replaces settings as a whole on edit, a patch that adds a missing settings key also sends the board's existing settings along with it, so its challenges and other settings are kept. boardManagerSettings is used as default values for board add config fields, and explicit CLI flags override these defaults.
The bundled preset file is src/presets/community-defaults.jsonc.
Wordfilters replace configured text in a publishing client before the
publication is signed. The community node then rejects any publication that
still contains a filtered source word. Consequently, enable wordfilters only
after the clients used by the board support the wordfilter/v1 contract. An
older client cannot publish the unfiltered text; it receives the configured
challenge error instead.
The bundled preset contains a commented example with 4chan's classic filters,
soy -> onions, tbh -> desu and smh -> baka, applied to post and
reply content, comment-edit content, and post titles. To enable it for a new
board:
- Install
@bitsocial/wordfilter-challenge@0.3.0on the bitsocial-cli instance that hosts the community. - Run
5chan board add ADDRESS --interactive-apply-defaults. - Choose Modify, uncomment the wordfilter challenge object, review the rule, then save the preset.
The active challenge object has this shape when configuring a custom preset or
editing an existing community's settings.challenges array:
{
"name": "@bitsocial/wordfilter-challenge",
"description": "Replaces configured words before publications are signed.",
"options": {
"wordfilter/v1/rules": "[{\"src\":\"soy\",\"dst\":\"onions\"},{\"src\":\"tbh\",\"dst\":\"desu\"},{\"src\":\"smh\",\"dst\":\"baka\"}]",
"wordfilter/v1/fieldNames": "[\"comment.content\",\"comment.title\",\"commentEdit.content\"]",
"error": "This board replaces certain words. Please retry after refreshing the board."
},
"publicOptions": [
"wordfilter/v1/rules",
"wordfilter/v1/fieldNames",
"error"
]
}The two contract options must remain in publicOptions: publishing clients
need them to produce text the community will accept. Field paths start with
the publication type (comment.content, commentEdit.content,
vote.author.displayName, ...), as required by wordfilter-challenge 0.3.0;
bare paths such as content are rejected when the configuration is saved, and
clients written against the 0.2.0 bare-path format do not apply prefixed paths,
so their posts are rejected until they are updated. Rules are literal and
case-insensitive substring matches, not regular expressions and not
whole-word matches. This example matches soy, Soy, and SOY, and also
the soy inside soybean (which becomes onionsbean, exactly as on 4chan),
but not s o y or Unicode lookalikes.
Invalid rules within one wordfilter challenge are rejected when the community
configuration is saved. Each challenge allows at most 64 rules; source and
destination strings are limited to 128 characters; and replacements that could
loop or reintroduce another source in that challenge are rejected. Conflicts
between separate wordfilter challenges cannot be validated in isolation; they
surface during client application or as a publication rejection. See the
@bitsocial/wordfilter-challenge contract
for the complete validation and client behavior.
The bundled preset sets settings.pages (configurable page sorts, pkc-js 0.0.108+) to only what 5chan reads, instead of pkc-js's nine default post sorts and five reply sorts:
- posts:
@pkcprotocol/active-page-sortpublishes the bump order under theactivekey 5chan reads (board, archive, catalog; the catalog's "reply count" and "creation date" orders are re-sorted client-side). It honourssageand the bump limit, see Feature 2. ItsbumpLimitoption is overwritten with the board's ownbumpLimitwhen the preset is applied and when the board manager starts. - replies:
old(thread view, oldest first, embedded) andnew(board-view reply preview, fetched on demand). 5chan requests these keys and flattens them client-side, so they are notoldFlat/newFlat. - Older 5chan clients: 5chan v0.10.0 and older (desktop and Android builds not yet updated) fetch the posts
newsort for the catalog's "creation date" option. To keep that option working for them, add{ "name": "new" }topages.posts(not preloaded, so it costs no record bytes).
Requires bitsocial page-sort install @pkcprotocol/active-page-sort@0.0.7 on the bitsocial-cli instance (0.19.116 or newer), see Installation.
Notes for operators:
- Any change to
settings.pagesmakes the community regenerate every CommentUpdate once, on its next pass; after that it is back to normal. - Boards added before this preset have no
settings.pages, so they keep pkc-js's default sorts and have no bump limit. The board manager never adds it on its own; add it with bitsocial-cli (below). - Threads archived at the bump limit by earlier versions stay archived.
To add the page sorts to an existing board, install the sort (see above), then edit the community with a JSON file. --jsonFile merges objects into the board's current settings, so its challenges stay, and it replaces arrays as a whole. Use the board's own bumpLimit (the board manager corrects it on start anyway):
cat > pages.json <<'JSON'
{
"settings": {
"pages": {
"posts": [{ "name": "@pkcprotocol/active-page-sort", "options": { "noBumpKeywords": "sage", "bumpLimit": "300" }, "preloaded": true }],
"replies": [{ "name": "old", "preloaded": true }, { "name": "new" }]
}
}
}
JSON
docker compose cp pages.json bitsocial:/tmp/pages.json
docker compose exec bitsocial bitsocial community edit <board-address> -f /tmp/pages.json5chan board add ADDRESS5chan board edit ADDRESS5chan board list5chan board remove ADDRESS5chan defaults set5chan help [COMMAND]5chan logs5chan start
Add one or more boards to the config
USAGE
$ 5chan board add ADDRESS... [--rpc-url <value>] [--per-page <value>] [--pages <value>] [--bump-limit
<value>] [--archive-purge-seconds <value>] [--apply-defaults] [--skip-apply-defaults] [--interactive-apply-defaults]
[--defaults-preset <value>]
ARGUMENTS
ADDRESS... Board address(es) to add (one or more, space-separated)
FLAGS
--apply-defaults Apply preset defaults silently (no prompts)
--archive-purge-seconds=<value> Seconds after archiving before purge
--bump-limit=<value> Bump limit for threads
--defaults-preset=<value> Path to a custom preset JSON file
--interactive-apply-defaults Interactively review and modify preset defaults before applying
--pages=<value> Number of pages
--per-page=<value> Posts per page
--rpc-url=<value> [default: ws://localhost:9138, env: PKC_RPC_WS_URL] PKC RPC WebSocket URL (for
validation)
--skip-apply-defaults Skip applying preset defaults
DESCRIPTION
Add one or more boards to the config
Multiple addresses may be supplied space-separated; the same defaults decision
and preset are applied to each. All addresses are validated and checked for
conflicts up front, so nothing is written if any address is invalid or already
present.
Preset defaults behavior:
--apply-defaults Apply all preset defaults silently (no prompts)
--skip-apply-defaults Skip preset defaults silently
--interactive-apply-defaults Review defaults, accept all, modify in $EDITOR, or skip (requires TTY)
Interactive TTY (no flags) Same as --interactive-apply-defaults: shows [A]ccept / [M]odify / [S]kip
Non-interactive (no flags) Errors; requires --apply-defaults or --skip-apply-defaults
When choosing [M]odify, the preset opens in your editor ($VISUAL > $EDITOR > vi/notepad).
Modified presets are validated before applying; invalid changes fail the command.
Note: "board add" only accepts 5chan settings flags (pagination, bump limits, archiving).
To set board settings (title, description, rules, etc.), use a WebUI or bitsocial-cli:
https://github.com/bitsocialnet/bitsocial-cli#bitsocial-community-edit-address
EXAMPLES
$ 5chan board add random.bso
$ 5chan board add random.bso tech.bso flash.bso
$ 5chan board add tech.bso --bump-limit 500
$ 5chan board add flash.bso --per-page 30 --pages 1
$ 5chan board add my-board.bso --rpc-url ws://custom-host:9138
$ 5chan board add my-board.bso --apply-defaults
$ 5chan board add my-board.bso other-board.bso --apply-defaults
$ 5chan board add my-board.bso --skip-apply-defaults
$ 5chan board add my-board.bso --interactive-apply-defaults
$ 5chan board add my-board.bso --apply-defaults --defaults-preset ./my-preset.json
See code: src/commands/board/add.ts
Edit 5chan settings for an existing board
USAGE
$ 5chan board edit ADDRESS [-i | --per-page <value> | --pages <value> | --bump-limit <value> |
--archive-purge-seconds <value> | --reset <value>]
ARGUMENTS
ADDRESS Board address to edit
FLAGS
-i, --interactive Open the board config in $EDITOR for interactive editing
--archive-purge-seconds=<value> Seconds after archiving before purge
--bump-limit=<value> Bump limit for threads
--pages=<value> Number of pages
--per-page=<value> Posts per page
--reset=<value> Comma-separated fields to reset to defaults (per-page, pages, bump-limit,
archive-purge-seconds, moderation-reasons)
DESCRIPTION
Edit 5chan settings for an existing board
This command configures how 5chan manages the board (pagination, bump limits, archiving).
Use --interactive (-i) to open the board config in $EDITOR for direct viewing/editing.
To edit board settings (title, description, rules, etc.), use a WebUI or bitsocial-cli:
https://github.com/bitsocialnet/bitsocial-cli#bitsocial-community-edit-address
EXAMPLES
$ 5chan board edit tech.bso --bump-limit 500
$ 5chan board edit flash.bso --per-page 30 --pages 1
$ 5chan board edit random.bso --reset per-page,bump-limit
$ 5chan board edit random.bso --per-page 20 --reset bump-limit
$ 5chan board edit random.bso --reset moderation-reasons
$ 5chan board edit random.bso --interactive
$ 5chan board edit random.bso -i
See code: src/commands/board/edit.ts
List all board addresses
USAGE
$ 5chan board list
DESCRIPTION
List all board addresses
EXAMPLES
$ 5chan board list
See code: src/commands/board/list.ts
Remove a board from the config
USAGE
$ 5chan board remove ADDRESS
ARGUMENTS
ADDRESS Community address to remove
DESCRIPTION
Remove a board from the config
EXAMPLES
$ 5chan board remove random.bso
See code: src/commands/board/remove.ts
Set global default settings for all boards
USAGE
$ 5chan defaults set [-i | --per-page <value> | --pages <value> | --bump-limit <value> | --archive-purge-seconds
<value> | --reset <value>]
FLAGS
-i, --interactive Open defaults in $EDITOR for interactive editing
--archive-purge-seconds=<value> Seconds after archiving before purge
--bump-limit=<value> Bump limit for threads
--pages=<value> Number of pages
--per-page=<value> Posts per page
--reset=<value> Comma-separated fields to remove from defaults (per-page, pages, bump-limit,
archive-purge-seconds, moderation-reasons)
DESCRIPTION
Set global default settings for all boards
Defaults in global.json apply to every board unless overridden per-board.
Use --interactive (-i) to open the defaults object in $EDITOR for direct editing.
EXAMPLES
$ 5chan defaults set --per-page 20
$ 5chan defaults set --bump-limit 500 --pages 10
$ 5chan defaults set --reset per-page,bump-limit
$ 5chan defaults set --per-page 20 --reset bump-limit
$ 5chan defaults set --interactive
$ 5chan defaults set -i
See code: src/commands/defaults/set.ts
Display help for 5chan.
USAGE
$ 5chan help [COMMAND...] [-n]
ARGUMENTS
[COMMAND...] Command to show help for.
FLAGS
-n, --nested-commands Include all nested commands in the output.
DESCRIPTION
Display help for 5chan.
See code: @oclif/plugin-help
View the latest 5chan daemon log file. By default dumps the full log and exits. Use --follow to stream new output in real-time (like tail -f).
USAGE
$ 5chan logs [-f] [-n <value>] [--since <value>] [--until <value>] [--logPath <value>] [--stdout |
--stderr]
FLAGS
-f, --follow Follow log output in real-time (like tail -f)
-n, --tail=<value> [default: all] Number of log entries to show from the end. Use "all" to show everything.
--logPath=<value> Specify the directory containing log files
--since=<value> Show logs since timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
42m, 2h, 1d)
--stderr Show only stderr log entries (output of pkc-logger library)
--stdout Show only stdout log entries
--until=<value> Show logs before timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
42m, 2h, 1d)
DESCRIPTION
View the latest 5chan daemon log file. By default dumps the full log and exits. Use --follow to stream new output in
real-time (like tail -f).
EXAMPLES
$ 5chan logs
$ 5chan logs -f
$ 5chan logs -n 50
$ 5chan logs --since 5m
$ 5chan logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z
$ 5chan logs --since 1h -f
$ 5chan logs --stdout
$ 5chan logs --stderr
$ 5chan logs --stdout -f
See code: src/commands/logs.ts
Start board managers for all configured boards
USAGE
$ 5chan start [-c <value>] [--log-path <value>]
FLAGS
-c, --config-dir=<value> Path to config directory (overrides default)
--log-path=<value> [default: /home/runner/.local/state/5chan] Directory to store daemon log files
DESCRIPTION
Start board managers for all configured boards
Board managers enforce imageboard-style thread lifecycle rules on each board:
- Archive threads that exceed board capacity (perPage × pages)
- Keep each board's @pkcprotocol/active-page-sort bump limit in sync with its bumpLimit
- Purge archived threads after the retention period expires
- Purge author-deleted threads and replies
The config directory is watched for changes; boards are hot-reloaded
(added, removed, or restarted) without requiring a full restart.
Daemon output is written to a rotated log file (default: /home/runner/.local/state/5chan).
View it with `5chan logs`. stderr is suppressed on the terminal; real
uncaught errors still reach the terminal.
EXAMPLES
$ 5chan start
$ 5chan start --config-dir /path/to/config
$ 5chan start --log-path /var/log/5chan
See code: src/commands/start.ts
5chan start watches the config directory (boards/ and global.json) using chokidar with a 200ms debounce, plus a periodic reconcile (every 2s) that guarantees changes are picked up even if the OS watcher drops events (e.g. inotify queue overflow). Changes that arrive while a reload is already running are queued and applied right after it. When any config file changes:
- Loads and validates the new config
- Diffs old vs new boards
- Stops board managers for removed boards
- Restarts board managers for boards with changed config
- Starts board managers for added boards
- Logs the delta:
config reloaded: +N added, -N removed, ~N changed, M running
This means you can add, edit, or remove boards while the board manager is running — either by editing config files directly or by running 5chan board add/edit/remove in another terminal. When global config changes (rpcUrl, defaults), all running boards are restarted.
Each board manager acquires a PID-based lock file ({statePath}.lock) to prevent concurrent board managers on the same board. On startup:
- Attempts to create lock file exclusively (
wxflag) - If lock exists, reads the PID and checks if it's still alive (
process.kill(pid, 0)) - If alive — throws
Another board manager (PID N) is already running - If stale — removes the old lock and retries
The lock is released when the board manager stops.
Archiving is driven by community.update events, because it depends on board activity (new threads pushing old ones past capacity). Purging is different: once a thread is archived, only the clock decides when its retention expires.
So the board manager also runs a periodic sweep that purges archived threads whose archivePurgeSeconds has elapsed, independent of update events. A board that goes quiet right after archiving would otherwise keep expired threads in the archive indefinitely, since nothing would trigger the update handler.
| Env var | Default | Purpose |
|---|---|---|
PURGE_SWEEP_INTERVAL_SECONDS |
60 |
How often to check for archived threads past their retention window. Set to 0 to disable the sweep and fall back to purging on update events only. |
The sweep does no network fetching — it reads the archived-thread timestamps already held in the state file, and only publishes when something is genuinely due. It is serialized against the update handler, so a thread is never purged twice concurrently.
The board manager detects comments and replies that were deleted by their author (where comment.deleted === true) and purges them via createCommentModeration({ commentModeration: { purged: true } }). Once purged, the comment is removed from the board and won't appear in future listings. If a purge hasn't been processed yet, the next cycle may re-publish a redundant purge moderation, which is a harmless no-op.
On startup for each board (using the internally-created PKC instance):
- Check state JSON for a signer private key for this board address
- If none exists, create one via
pkc.createSigner()and save to state JSON - Check board roles via
community.rolesfor the signer's address - If not a mod, auto-add via
community.edit()(works because we run onLocalCommunityorRpcLocalCommunity— we own the board)
Logged via pkc-logger when creating signer or adding mod role.
When bitsocial-cli changes a board's address (e.g., from a hash like 12D3KooW... to a named address like random.bso), the board manager detects the change automatically via the pkc-js update event and migrates all associated files:
- Signer key is moved to the new address in the state file
- Board directory is renamed from
boards/{oldAddress}/toboards/{newAddress}/(with updatedaddressfield inconfig.json) - Lock file is re-acquired for the new address
- Internal maps are updated so moderation continues uninterrupted
The mod signer carries over to the new address — no need to re-assign the moderator role. If the migration fails (e.g., lock conflict on the new address), the board manager logs the error and continues operating under the old address.
State is stored as state.json inside each board's directory (boards/{address}/state.json), alongside the board's config.json.
{
"signers": {
"<boardAddress>": { "privateKey": "..." }
},
"archivedThreads": {
"<commentCid>": { "archivedTimestamp": 1234567890 }
}
}signers: maps board address → mod signer private key (auto-created if missing)archivedThreads: maps comment CID → archive metadata (entries removed on purge)- State writes use atomic temp-then-rename to prevent corruption
- Loaded on startup, written on archive, entries removed on purge
The script may start long after the board has been running. On first run, many threads may need locking/purging at once. No rate limiting is needed — same-node publishing has no pubsub overhead. The first cycle may be heavier; steady-state handles a few threads per update.
Before archiving, checks the thread's archived property. Skips if already archived (pkc-js throws on duplicate moderation actions).
Uses pkc-logger (same logger as the pkc-js ecosystem). Key events logged:
- Board manager start/stop
- Threads archived (with CID and reason)
- Bump limit written into
@pkcprotocol/active-page-sort, or that it has no effect on a board without the sort - Threads purged
- Author-deleted comments purged
- Config hot-reload events
- Mod role auto-added
- Errors
Daemon log file. When 5chan start runs, stdout and stderr are captured to a
rotated log file under $XDG_STATE_HOME/5chan/log/ (inside Docker: /data/5chan/log/).
Each entry is prefixed with [ISO-timestamp] [stdout|stderr]. At most 5 files are
retained; the oldest is deleted on each restart. Each file is capped at 20MB.
stderr is suppressed on the terminal — only real uncaught errors reach it. Use
5chan logs (or 5chan logs -f) to view daemon output:
docker compose exec 5chan 5chan logs # dump latest log
docker compose exec 5chan 5chan logs -f # stream live
docker compose exec 5chan 5chan logs --stderr -f # only pkc-logger outputDocker: debug logging is off by default. The example compose files set
DEBUG=bitsocial:5chan-board-manager* on the 5chan service so daemon output
flows into the log file. Because stderr is no longer teed to the terminal,
docker logs will be quieter than before — use 5chan logs to see the full
debug stream. To silence the log file, drop the variable from your compose
file (or override it to empty):
environment:
DEBUG: ""To broaden the captured namespaces (e.g. to include pkc-js events), widen DEBUG:
environment:
DEBUG: "bitsocial:5chan-board-manager*,pkc*,pkc-js*"5chan start exits with status 1 if an established RPC connection drops, even
if the socket reconnects: the SDK's previous community subscriptions are no
longer valid. The example Docker Compose files use restart: unless-stopped,
so Docker starts a fresh daemon with fresh subscriptions and the existing
signers/archive state. An unavailable RPC at startup also triggers this restart
path. RPC startup is limited to 30 seconds, and shutdown is limited to 10 seconds
if SDK work or cleanup is stuck. Deployments outside the example Compose files
must provide an equivalent process restart policy.
Each board manager runs a periodic heartbeat that:
- Logs a line per tick at trace level (
[board <addr>] heartbeat — last update: <ISO> (<n>s ago)) so you can confirm the daemon is alive even when a board is idle. Visible with the defaultDEBUG='bitsocial:5chan-board-manager*'. - Touches a shared heartbeat file (default
$XDG_STATE_HOME/5chan/heartbeat, i.e./data/5chan/heartbeatin Docker) for the Docker healthcheck to consume. - If
now - lastUpdateAtexceedsHEARTBEAT_STALE_UPDATE_SECONDS, an error-level[board <addr>] no update events for <n>sline is logged each tick. An idle board alone does not trigger a restart.
| Env var | Default | Purpose |
|---|---|---|
HEARTBEAT_INTERVAL_SECONDS |
300 |
Tick cadence. |
HEARTBEAT_STALE_UPDATE_SECONDS |
1800 |
If no community.update event has fired within this window, a stale-update warning is logged each tick. |
HEARTBEAT_FILE |
<log-path>/heartbeat |
Override the heartbeat file path (e.g. for tests). |
The example compose files include a healthcheck that reports unhealthy if the heartbeat file's mtime is older than 10 minutes — useful for docker ps and external watchdogs. The heartbeat checks timer liveness; RPC disconnect recovery works independently of it. A connected server that stops delivering updates still requires investigation of the stale-update warning.
Total thread capacity = per_page × pages. Both are configurable per board.
| Setting | Range | Description |
|---|---|---|
per_page |
15–30 | Threads per index page (e.g., /b/ = 15, /v/ = 20, /f/ = 30) |
pages |
1–10 | Number of index pages (e.g., /f/ = 1, most boards = 10) |
Capacity examples: /b/ = 150, /v/ = 200, /f/ = 30.
- New thread created → sits at top of page 1
- Bumped by replies → moves back to top of page 1
- Sinks gradually as newer threads get replies
- Falls off last page → archived (read-only, ~48h)
- Purged → permanently deleted from 4chan's servers
A reply moves the thread to the top of page 1. This is equivalent to PKC's "active sort", which orders threads by lastReplyTimestamp. 5chan boards run @pkcprotocol/active-page-sort under the same key, which also skips sage replies and replies past the bump limit.
Configurable per board (300–500+). After N replies, new replies no longer bump the thread, but it still accepts replies until it falls off the last page.
Examples: /b/ = 300, /3/ = 310, /v/ = 500.
Sit at top of page 1, exempt from thread limit and archiving. "Pinned" and "sticky" are the same thing.
- Archived = locked/read-only, still visible for ~48 hours
- Purged = permanently deleted from 4chan's servers
- Not all boards have archives (
is_archivedflag in API)
External services (archive.4plebs.org, desuarchive.org) independently scrape and preserve threads before purge.
image_limit, max_filesize, max_comment_chars, cooldowns, spoilers, country_flags, user_ids, forced_anon, etc.
Unit tests run under Vitest:
npm testEnd-to-end tests run the real board manager against a real PKC RPC server:
npm run test:e2eThe e2e suite spawns its own Kubo (IPFS) daemon on random ports and an in-process PKC RPC server on port 19138. All state is written to temp directories that are removed at teardown. No bitsocial-cli or docker-compose stack needs to be running. On first run the suite downloads the pinned Kubo release from GitHub, checks it against a pinned SHA-512 and caches it under node_modules/.cache/kubo; set KUBO_BINARY to use an existing ipfs binary instead.
To point the suite at an externally-running RPC instead (e.g. a long-lived docker-compose stack), set PKC_RPC_WS_URL before invoking the script — when set, global-setup skips spawning and uses the provided URL verbatim:
PKC_RPC_WS_URL=ws://localhost:9138/YOUR-AUTH-KEY npm run test:e2eThe in-process RPC server registers @pkcprotocol/active-page-sort (a devDependency) for the bump-limit tests; an external RPC server needs it installed (bitsocial page-sort install @pkcprotocol/active-page-sort@0.0.7).
| Behavior | 4chan | This module |
|---|---|---|
| Bump limit | Threads past bump limit still accept replies — they just stop rising in the catalog | Same, applied by @pkcprotocol/active-page-sort. A board that does not run the sort has no bump limit |
| Sage | Replying with sage in the email field prevents the thread from being bumped |
A reply whose content has a line equal to sage does not bump the thread, applied by @pkcprotocol/active-page-sort |
| Image limit | Per-thread image limit (e.g., 150 images on /b/) after which no more images can be posted | Not implemented — pkc-js has its own file-size constraints but no per-thread image count limit |
External module using pkc-js's public API:
- No pkc-js core modifications needed
- Uses
pkc.createCommentModeration()for both archiving and purging - Listens to board
updateevents (viacommunity.on('update', ...)) to detect new posts - Gets thread positions from the board's
activesort, embedded (community.posts.pages.active) or by CID (community.posts.pageCids.active), in published order; only a board without anactivesort falls back to sorting its preloaded page bylastReplyTimestampdescending (thenpostNumber)
Uses 4chan field names for interoperability.
| Setting | Default | 4chan range | Description |
|---|---|---|---|
per_page |
15 | 15–30 | Threads per index page |
pages |
10 | 1–10 | Number of index pages |
bump_limit |
300 | 300–500 | Replies after which new replies stop bumping the thread; written into @pkcprotocol/active-page-sort's bumpLimit option |
archive_purge_seconds |
172800 (48h) | ~48h | Seconds before archived posts are purged (no 4chan equivalent, 4chan uses ~48h) |
moderationReasons |
(see below) | — | Reason strings passed to createCommentModeration(). Fields: archiveCapacity, purgeArchived, purgeDeleted, purgeCapacity (archiveBumpLimit and lockBumpLimit are deprecated and ignored) |
Max active threads = per_page × pages (default: 150)
Cannot do community.posts.getPage("active"). Must either:
- Use the embedded
community.posts.pages.active, orcommunity.posts.pageCids.activeto get the CID and fetch that page, followingnextCidin both cases - Or, when the board publishes no
activesort, fall back to sorting preloaded pages bylastReplyTimestampdescending, thenpostNumber(approximates the built-in active sort; ignores sage and the bump limit)
The entire board IPFS record is capped at 1MB (MAX_FILE_SIZE_BYTES_FOR_COMMUNITY_IPFS). community.posts.pages.hot is preloaded into the record with whatever space remains after the rest of the record (title, description, roles, challenges, etc.).
- If the preloaded page has no
nextCid, it contains all posts — no pagination needed - If
nextCidis present, additional pages must be fetched viacommunity.posts.getPage({ cid: nextCid }) community.posts.pageCids.activeprovides the CID of the first active-sorted page, which is the sort order the board manager needs
Reference: pkc-js/src/community/community-client-manager.ts, pkc-js/src/runtime/node/community/local-community.ts
1. Create PKC instance internally from the provided pkcRpcUrl
2. Load state JSON; get or create signer for this board via `pkc.createSigner()`
3. Get board (`LocalCommunity` or `RpcLocalCommunity`)
4. Check board roles via `community.roles`; if missing, call `community.edit()` to add as mod
5. Acquire file lock to prevent concurrent board managers on same board
6. Call `community.update()`
7. On the first 'update' event that carries the board's settings (once per start):
- If `settings.pages.posts` names `@pkcprotocol/active-page-sort` and its `bumpLimit` option differs from `bump_limit`, `community.edit()` it (sending the whole settings object); log a warning and carry on if the edit fails
- After a successful edit, skip step (d) until an update carries a record whose `updatedAt` is more than one past both the pre-edit record's `updatedAt` and the edit time
- Otherwise log that `bump_limit` has no effect on this board
8. On each 'update' event:
a. Determine thread source (three scenarios):
1. pages.active or pageCids.active exists → use it in published order, paginate via nextCid
2. No active sort → use the preloaded page, sort by lastReplyTimestamp desc then postNumber
3. Neither exists → no posts, return early
b. Walk through pages to build full ordered list of threads
c. Filter out pinned threads
d. For each non-pinned thread beyond position (per_page * pages):
- Skip if already archived
- createCommentModeration({ archived: true }) and publish
- Record archivedTimestamp in state file
e. For each archived thread where (now - archivedAt) > archive_purge_seconds:
- createCommentModeration({ purged: true }) and publish
- Remove from state file
f. For each author-deleted comment/reply:
- createCommentModeration({ purged: true }) and publish
9. Every PURGE_SWEEP_INTERVAL_SECONDS, independent of update events (skipped when set to 0):
- Run step (e) again, so time-based purging still happens on a quiet board
| API | Purpose |
|---|---|
pkc.createCommentModeration() |
Archive and purge threads |
commentModeration.publish() |
Publish the moderation action |
community.posts.pageCids.active |
Get active sort page CID |
community.posts.pages.hot |
Preloaded first page (for calculating active sort) |
community.on('update', ...) |
Listen for new posts/updates |
page.nextCid |
Paginate through multi-page feeds |
| File | Relevant code |
|---|---|
src/pkc/pkc.ts |
createCommentModeration() definition |
src/publications/comment-moderation/schema.ts |
ModeratorOptionsSchema with archived, purged fields |
src/runtime/node/community/local-community.ts |
Existing archived check that blocks replies |
src/runtime/node/community/db-handler.ts |
queryPostsWithActiveScore() — active sort CTE |
src/pages/util.ts |
Sort type definitions and scoring functions |