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

nodebb-plugin-usercleaner

v1.0.1

Published

Bulk-delete dormant, abandoned and spam accounts from NodeBB with a dry-run-first admin workflow, live filter preview and background deletion jobs.

Readme

nodebb-plugin-usercleaner

Bulk-delete dormant, abandoned and spam accounts from a NodeBB forum, from an admin page built around a dry-run-first workflow.

Written for forums that have accumulated tens of thousands of registrations, most of which never posted and never came back.

Safety model

Deleting users is irreversible, so the plugin is deliberately hard to fire by accident:

  • Dry run is on by default and resets itself to on after every real run. A dry run performs the full scan and reports exactly what would be deleted, then stops.
  • A preview is required before a real run can be started.
  • Two-part confirmation: the admin must type both the exact number of matched accounts and the word DELETE.
  • The count is re-checked server-side. The job rescans before deleting anything; if the number of matching accounts changed since the preview, the run aborts without deleting. Preview again and re-confirm.
  • Always protected, whatever the filters say: administrators, global moderators, category moderators and the admin running the job.
  • Overly broad filter sets are rejected. Post count and reputation ceilings alone are not enough — at least one narrowing condition (last online, account age, unconfirmed email, never logged in, profile signals, an email pattern, flags, banned state or a group) is required.
  • Per-run deletion cap (default 1000) as a final valve.
  • Every deletion is written to the ACP event log, as is the run itself.

Filters

| Filter | Notes | |---|---| | Maximum post count | Default 0 — users who never posted. | | Maximum reputation | Default 0. | | Maximum topic count | Optional, off by default. | | Last online longer ago than | 1m / 3m / 6m / 1y / 2y / 3y / 5y / 10y. Accounts that never logged in are compared against their registration date. | | Account older than | Protects recent registrations from a broad rule. | | Email never confirmed | The strongest single spam signal on a public forum. | | Registered and never came back | lastonline never advanced past joindate. | | Email address matches | Case-insensitive regular expression, e.g. gsavps|pattaya-mega|verifiedlinklist. See the note below. | | Profile filled in | Full name, signature, about-me or an uploaded avatar — a profile-spammer with zero posts. | | Banned accounts | Include / only / exclude. | | Minimum flag count | Accounts that have been reported. | | Group allowlist | Only consider members of these groups. | | Group denylist | Never delete members of these groups (donators, verified members, …). |

Matching unconfirmed email addresses

With NodeBB's default emailVerification: 'send', a registration's address is not written to user:<uid>.email — it only lives on confirm:byUid:<uid> → confirm:<code> until the link is clicked, which is why the ACP user profile shows no address for these accounts. The email pattern filter reads both sources, so it matches the spam-domain registrations that never validated. The preview table and the CSV export show the address it matched; emailPending in the CSV marks the ones taken from the confirm object.

Content handling

Each run chooses what happens to content belonging to matched accounts:

  • Purge (default) — user.delete: the account and all of its posts, topics and uploads are removed.
  • Keep — user.deleteAccount: the account is removed, its content remains and is attributed to a guest.

With the default post-count filter of 0 the choice is moot, since matched users have no content.

Running at scale

Scanning and deleting run as a background job, because purging tens of thousands of accounts takes far longer than an HTTP request may last. The admin page polls for live progress and can cancel a run in flight; already-deleted accounts are not restored by a cancellation.

Deletions run through a small worker pool — Parallel deletions (1-20, default 5) in the deletion card — with a short pause per worker so a large run does not starve the forum. This is the same mechanism the ACP user list uses when you delete a selection: NodeBB has no bulk-delete endpoint, its user list simply fires one DELETE /users/:uid per selected account in parallel. Raise the value for faster runs; lower it if the forum feels sluggish while a run is going.

Job state lives in the database, not in the Node process, so it survives page reloads and is the same on every worker of a clustered NodeBB — the ACP poll can land on any worker and still sees the running job, and Cancel works from any of them. The progress bar sits at the top of the page and is rebuilt from that state on every load.

If the worker running a job dies (a restart or a crash mid-run), the record stops being updated; after a minute the page reports the job as lost and a new run can be started. Accounts already deleted stay deleted.

Preview vs. dry run. Preview is a synchronous scan that answers "how many match?" and fills the sample table; it is what arms the Run button. Dry run is the same scan executed as the background job, with the live progress bar and a downloadable CSV of every match, and it still deletes nothing. Only unticking Dry run and confirming twice deletes anything.

The matched account list can be downloaded as CSV after a dry run — review it before committing to a real run.

Install

cd /path/to/nodebb
npm install nodebb-plugin-usercleaner
./nodebb build
./nodebb restart

Activate under Admin → Extend → Plugins, then rebuild and restart once more. The page is at Admin → Plugins → User Cleaner.

Access requires the admin:users admin privilege.

Compatibility

NodeBB v4.14.0 or newer. The ACP template uses the {{tx(...)}} translation helper, which landed in v4.14.0; v3 and earlier are not supported at all, as the plugin uses the v4 ACP module loading pattern (plugin.json modules with ES-module syntax).

No theme dependency — the plugin only adds an ACP page.

Localisation

Strings live in languages/en-GB/usercleaner.json under the usercleaner namespace.

Note the two different call sites, which use different syntax:

  • Templates must use the benchpress helper: {{tx("usercleaner:filters.heading")}}, or {{tx(./label)}} for a token supplied as template data. NodeBB v4.15.0 removed whole-page translation (248257914e), so a bare [[usercleaner:key]] left in a .tpl now renders literally.
  • Client JS still takes [[usercleaner:key]] directly — alerts.*, modals.dialog and $.fn.translateText all run the string through the translator themselves.

Development

cd /path/to/nodebb
npm install /path/to/nodebb-plugin-usercleaner
./nodebb build && ./nodebb dev
npm run lint   # in the plugin directory

Licence

MIT