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-backblaze-b2-s3-uploads

v0.2.2

Published

Stores forum uploads in Backblaze B2 via the S3-compatible API. Serves files through a permission-aware proxy with presigned URLs.

Readme

nodebb-plugin-backblaze-b2-s3-uploads

License: MIT Buy Me A Coffee

Stores NodeBB forum uploads in Backblaze B2 through the S3-compatible API. Files are served through a permission-aware proxy that issues short-lived presigned URLs, so:

  • Uploads in public categories are reachable by anonymous visitors (still through the proxy, which checks topics:read for guests).
  • Uploads in private categories require the visitor to have topics:read on that category.
  • Direct B2 URLs are never put into post content — only opaque proxy URLs are, so leaked links die with the user's session/permission.

How it works

  1. User uploads a file via the composer (post body) or as a topic thumbnail. Plugin intercepts filter:uploadFile / filter:uploadImage for folder='files', streams the file to B2, stores {uid, name, type, size} keyed by the B2 object key in NodeBB's database.
  2. Post content references the file as /uploads/b2/<key>.
  3. On action:post.save / action:post.edit, the plugin diffs the post's previously-tracked keys against the URLs in current content. New references get linked to {pid, tid, cid}; references that disappeared get re-orphaned (so the cleanup sweep eventually removes them).
  4. On action:topic.post, B2 paths in topic.thumbs are linked to the topic's main post — same lifecycle as body URLs.
  5. When a browser requests /uploads/b2/<key>, the plugin checks privileges.categories.can('topics:read', cid, uid), generates a fresh presigned URL with TTL from settings, and 302-redirects.
  6. The 302 is Cache-Control: private, max-age=(TTL - 60s) so the browser doesn't hit NodeBB on every image load.

What stays local

System uploads — profile pictures, group covers, category icons — pass through to NodeBB's native local storage (via file.saveFileToLocal). Their lifecycle is wired into NodeBB core (e.g. old avatar deleted on change, group cover removed when group is deleted), so trying to mirror that into B2 would create a separate set of leaks. The cost saving from offloading them is negligible compared to post attachments.

Cleanup lifecycle

  • Composer abandons (file uploaded, post never submitted) → entry sits in b2-uploads:orphans, sweep deletes it after cleanupAgeHours (default 24h).
  • Edit removes a file from a post → re-orphaned, swept on next cycle.
  • Post purged (action:post.purge) → file deleted from B2 immediately, all DB references removed. Cascades from topic.purge and user.delete.
  • Soft-delete (post hidden but not purged) → file kept; allows restore.

Profile & GDPR integration

NodeBB's native profile uploads tab (/user/<slug>/uploads) and GDPR data export (Settings → Data Export → Uploads) only see files stored on the local disk. Without integration, files uploaded through this plugin are invisible to both.

This plugin patches both:

  • Profile uploads tab — hooks filter:account/uploads.build and merges B2 entries into the rendered list. Each entry links back through /uploads/b2/<key>, so the same permission check + Bunny/B2 redirect runs as on regular post images. The sidebar's "Uploads" count is also augmented via filter:helpers.getUserDataByUserSlug.
  • GDPR uploads export — patches usersAPI.generateExport to handle type=uploads in-process. The resulting ZIP contains native uploads (under their original paths) plus a b2/ folder with all the user's B2 files. posts and profile exports are untouched and still go through NodeBB's native fork.

If the patch can't be installed (NodeBB internals change in a future major), the plugin logs a warning and the export falls back to the native fork — i.e. you lose B2 files in the export, but native uploads still work.

Install

npm version npm downloads license

cd /path/to/nodebb
npm install nodebb-plugin-backblaze-b2-s3-uploads
./nodebb activate nodebb-plugin-backblaze-b2-s3-uploads
./nodebb build
./nodebb restart

Cloudron

Open the NodeBB app's Web Terminal and run:

cd /app/code
/usr/local/bin/gosu cloudron:cloudron npm install nodebb-plugin-backblaze-b2-s3-uploads
/usr/local/bin/gosu cloudron:cloudron ./nodebb build

Then restart the app from the Cloudron Dashboard. Activate via ACP → Extend → Plugins.

Local development

cd /path/to/nodebb-plugin-backblaze-b2-s3-uploads && npm install
cd /path/to/nodebb && npm install /path/to/nodebb-plugin-backblaze-b2-s3-uploads
./nodebb activate nodebb-plugin-backblaze-b2-s3-uploads
./nodebb build
./nodebb restart

Configure

Open ACP → Plugins → Backblaze B2 Uploads (S3 API) and fill in the fields:

ACP settings page

| Field | Example | Notes | |---|---|---| | S3 Endpoint | https://s3.eu-central-003.backblazeb2.com | From your B2 bucket details ("S3 API") | | Region | eu-central-003 | The bit between s3. and .backblazeb2.com | | Bucket name | forum-uploads | Bucket must be Private | | Application Key ID | 0035… | Use a key restricted to this single bucket | | Application Key | K003… | Stored in NodeBB settings DB | | Path prefix | uploads | All keys go under this prefix | | Presigned URL TTL | 600 | Seconds. 600 = 10 min. Max 604800 (7 days). | | Max file size | 26214400 | 25 MB default | | Max video size | 104857600 | 100 MB default | | Allowed extensions | jpg,jpeg,png,… | Comma separated |

Heads-up: NodeBB has its own global upload limit in Settings → Uploads. The lower of the two wins.

Recommended B2 setup

  1. Create a Private bucket.
  2. Create an Application Key scoped to only that bucket, with read+write permissions.
  3. Optionally add a CORS rule allowing your forum origin (only matters if you add direct browser uploads later — not needed for the proxy flow).

Optional: Bunny CDN delivery

By default the proxy redirects to a B2 presigned URL — every visitor pulls bytes straight from B2. If you want edge caching, image optimization and global low-latency delivery, enable Bunny CDN.

How it works

Browser → /uploads/b2/<key>            (NodeBB proxy: permission check)
        ← 302 https://<zone>.b-cdn.net/<key>?token=...&expires=...
Browser → Bunny edge POP (verifies token, serves from cache or pulls from B2)
        ← bytes

The proxy still runs every request — permission checks remain authoritative. Only the bytes are delivered through Bunny.

Bunny dashboard setup

  1. Pull Zones → Add Pull Zone
    • Origin Type: Backblaze B2
    • Application Key ID + Application Key (from B2; can be the same key the plugin uses)
    • Bucket name + region
  2. Settings → Security → Token Authentication: ON
  3. Copy the Token Authentication Key shown there
  4. (optional) Optimizer: ON for auto WebP/AVIF + image resize via URL params
  5. (optional) Settings → Caching: tune to taste

Plugin settings

In the Bunny CDN delivery section of the ACP page, fill:

| Field | Example | |---|---| | Use Bunny CDN delivery | ☑️ | | Bunny zone hostname | omdm-uploads.b-cdn.net | | Token Authentication Key | (from Bunny dashboard) | | Bunny token TTL | 0 (= same as B2 TTL) or custom |

Tradeoffs

  • B2 → Bunny transfer is free (Bandwidth Alliance) — origin pulls cost nothing
  • Bunny → user transfer is paid but cheap (~$0.01/GB)
  • Each visitor still hits the NodeBB proxy first for permission check — no cache bypass on auth

Maintenance: orphan cleanup

Files uploaded but never used in a saved post become orphans (e.g. user opens the composer, drops an image, then closes the tab without submitting). The plugin tracks every upload in a sorted set b2-uploads:orphans; the entry is removed on action:post.save / action:post.edit once the upload is referenced from a saved post.

A background sweep runs every cleanupIntervalHours (default 6h). Anything still in the orphans set older than cleanupAgeHours (default 24h) is deleted from B2 and from the database.

ACP fields:

| Field | Default | Notes | |---|---|---| | Enable scheduled orphan cleanup | ON | Disables the background timer when off | | Delete orphans older than (hours) | 24 | Generous grace window — covers slow drafts | | Sweep interval (hours) | 6 | How often the timer fires |

There is also a Run cleanup sweep now button in the ACP that triggers the sweep immediately and reports scanned / deleted / failed counts.

Known limitations

  • Topic thumbnails added or removed via the API after the topic is createdThumbs.associate/Thumbs.delete don't fire NodeBB hooks, so changes outside the initial topic creation flow aren't tracked. Removing a thumbnail this way will leak the file in B2 until manual cleanup.
  • Multi-reference (same B2 URL pasted into a second post) — tracking is last-write-wins. Purging the most recently saved post will delete the file even if another post still references it. Rare in practice but the second post's image will 404.
  • Delete button in the profile uploads tab is a no-op for B2 entries — the native deletion flow is wired to local-disk paths only. Use post edit/purge to remove B2 files.

These are slated for the v0.3.0 milestone (proper reverse index b2-upload:<key>:pids + monkey-patch coverage for the missing hook points).

Roadmap

  • [ ] Reverse index for multi-reference correctness
  • [ ] Monkey-patch Thumbs.associate/delete so post-creation thumbnail edits track properly
  • [ ] B2-aware delete in the profile uploads tab
  • [ ] Migration command for existing local uploads (with dry-run mode)
  • [ ] Per-user / per-group upload quotas
  • [ ] Optional IP-bound Bunny tokens

Support

This plugin is free, MIT-licensed, and maintained in spare time. No ads, no trackers, no "pro tier" paywall. If it solved a real problem for your forum — cut your cloud storage bill by moving off S3, saved a disk partition from filling up with user uploads, or kept your private-category attachments actually private while still serving them fast through a CDN — a coffee is a nice way to say thanks.

☕ Buy me a coffee

Not required, ever. Issues and PRs are always welcome regardless.

License

MIT