About

Self-hosted, and there is nothing to run

"Self-hosted" is the word on the box of most tools people compare this one to, and it means a service: a container, a database, a version to track, a migration to run and a changelog to read. bugbottle has none of those. There is no bugbottle server, no admin interface and no data store to operate, because the whole backend half is two exported functions and a route you already have somewhere. What you self-host is your own endpoint — and the question this page answers is what that actually costs, and what you take on when you write the route yourself.

The figures below are from the repository on 28 September 2026 and are checked by npm run check; follow the links rather than the numbers if you are reading this later.

What you run #

Three shapes, in the order most people end up in them. None of them requires installing anything on the server beyond what the application already has.

A route in an application you already run. This is the whole of it:

ts
import { fileStore, handleReport } from "bugbottle/server";

const reports = fileStore({ dir: "./reports", maxReports: 2000 });

export const POST = (req: Request) => handleReport(req, { store: reports.store });

handleReport is a Request in and a Response out, so that line is a route in Hono, Cloudflare Workers, Deno, Bun, Next.js, Astro or anything else that speaks fetch — and expressHandler and fastifyHandler exist for the two that do not. The receiving guide is the long version, and the payload is what arrives.

A small container you own. examples/inbox is that route plus a read-only list and a detail page behind one password, with a Dockerfile and a compose.yml that put it on a VPS in front of Caddy. It is an example you copy, not a service we run: no database, one directory of files on a volume, and INBOX_PASSWORD set or it refuses to start. It also posts to Slack, Discord, Teams, Sentry, Jira, GitLab, Linear, Resend or SMTP from environment variables alone, so the notification is not something you have to write.

WordPress, in one activation. The plugin is the panel and a receiving endpoint in the same zip: mount the panel with one setting, and the site's own REST route takes the report, with its own authentication and its own rate limit. There is nothing else to deploy.

What it costs to operate #

The honest answer is one directory and a cron entry, and the numbers are constants you can read in the source:

Five things that bite whoever receives the report #

These are the same five on every framework page, collected here because they are properties of your deployment and not of the framework. All five are invisible until a report with a screenshot arrives, which is the worst time to find them.

What What actually happens Where
The framework's body limit is smaller than a report express.json() caps the body at 100 kB and Nest inherits it; Fastify's default bodyLimit is 1 MiB. A report with a screenshot is 2 MiB of base64, so the request is refused before your route runs and the handler never answers Express, Fastify, NestJS
A signed route behind a body parser A signature covers the exact bytes, and a JSON parser has already replaced them with an object. The bytes are usually still there (req.rawBody), just not where the adapter looks Express, Fastify
Behind a proxy, the rate limit counts the proxy trustProxy is false by default, and the address the limit keys on is the connection, not the header. One shared egress address is one shared bucket handleReport
A screenshot is checked in its bytes, not its declared type The decoded data URL is verified against the PNG signature before it is written; one that is not a picture is dropped and the report is stored without it Screenshots
Everything from the browser is hostile input The store receives lengths, null-byte-free strings and a validated body, because a report is about to be written to disk and read back into a page Validation

What you do not get #

This is the part to read before you choose it over a product, and the comparison says the same thing row by row with every figure sourced:

When it is the right choice #

Choose it when the reports are a by-product of something you already run: you have an API, a database and an inbox, and what is missing is the few kilobytes that turn "it's broken" into JSON you can act on. The panel is about 11.5 kB gzipped and the core about 1.4 kB, so the whole cost of the library is smaller than one image on the page that reports it.

Choose a self-hosted product when the reporters are not developers and what unblocks them is a queue with an owner, or when you want alerting, release health and a dashboard you did not write. That is a real need and this library is not the answer to it — the comparison page names the tools that are, and says what each one costs. Choose the WordPress plugin if the application is WordPress and nobody wants to write a route.