# bugdeck — every document in one file
> Open-source Marker.io / BugHerd alternative: a self-hosted visual bug-report widget for
> React apps. Screenshot or element pick, annotate, send — the report lands on your own
> server, behind your own auth, and is filed into your issue tracker.
Generated from the sources below by `scripts/build-llms-full.mjs`; edit those, never this.
The short index with links to each file on its own is `llms.txt`.
- README.md
- packages/widget/README.md
- packages/server/README.md
- packages/core/README.md
- CONTRIBUTING.md
- CHANGELOG.md
---
# README.md
## bugdeck
> **Open-source Marker.io / BugHerd alternative — self-hosted bug reports with screenshot, annotation & element picker. Files into Plane, GitHub, Linear.**
Reading this as an AI agent? [`llms.txt`](https://github.com/aitofy-dev/bugdeck/blob/main/llms.txt) is the index; [`llms-full.txt`](https://github.com/aitofy-dev/bugdeck/blob/main/llms-full.txt)
is every document in this repository as one file. See [For AI coding agents](#for-ai-coding-agents).
**v0.2 ships the Plane and GitHub adapters.** Linear is next — see [Roadmap](#roadmap).
### Why
Your users can already tell you what is broken. What they cannot do is tell you **which element**,
at **which viewport**, on **which build**, after **which failed request** — so every bug report
costs a round trip that starts with "can you send a screenshot?".
The capture layer that fixes this is what Marker.io, BugHerd and Userback charge $39–149/month for,
and it is the whole of this repository. The reports land on **your** server, in **your** tracker,
with **your** auth in front of them.
| | Marker.io | BugHerd | Userback | Jam.dev | FasterFixes | **bugdeck** |
|---|---|---|---|---|---|---|
| Price | from $39/mo | from $49/mo | from $49/mo | free tier, then per-user | free | **free** |
| Self-hosted | ✗ | ✗ | ✗ | ✗ | ✓ | **✓** |
| Open source | ✗ | ✗ | ✗ | ✗ | ✓ | **✓ MIT** |
| Screenshot | ✓ | ✓ | ✓ | ✓ | ✓ | **✓** |
| Annotate | ✓ | ✓ | ✓ | ✓ | ✓ | **✓ pen · box · arrow · crop** |
| Element picker | ✓ | ✓ | ✓ | ✓ | ? | **✓** |
| Drafts survive a refresh | ? | ? | ? | ? | ? | **✓ one per page** |
| Plane | ✗ | ✗ | ✗ | ✗ | ✗ | **✓** |
| GitHub | ✓ | ✓ | ✓ | ✓ | ✓ | **✓** |
| Linear | ✓ | ✓ | ✓ | ✓ | ✓ | *planned* |
Prices are the published entry tiers at the time of writing. `?` means the vendor does not document
it either way — corrections welcome as a PR.
### 30-second quick start
**1. The widget**, in your React app:
```bash
pnpm add @aitofy/bugdeck # react and react-dom >= 18 are peers
```
```tsx
import { FeedbackWidget } from '@aitofy/bugdeck';
;
```
That is the whole integration: a floating button, bottom right.
**2. The server**, anywhere Node runs:
```bash
PLANE_BASE_URL=https://plane.example.com \
PLANE_API_KEY=plane_api_xxxxxxxx \
PLANE_WORKSPACE_SLUG=acme \
PLANE_PROJECT_ID=00000000-0000-0000-0000-000000000000 \
npx @aitofy/bugdeck-server
```
Or GitHub Issues, three variables instead of four:
```bash
TRACKER=github GITHUB_OWNER=acme GITHUB_REPO=app \
GITHUB_TOKEN=ghp_xxxxxxxx \
npx @aitofy/bugdeck-server
```
Either way it listens on `:3131` — with Plane it resolves your board's columns and prints the map
first. Reports are SQLite rows and PNGs on disk; the issue appears in the tracker a second after
the user presses Send.
Already have a server? Mount the app in it and keep your own auth — four lines, in
[`@aitofy/bugdeck-server`](https://github.com/aitofy-dev/bugdeck/blob/main/packages/server/README.md#30-seconds). Docker Compose and every environment
variable are there too.
### How it works
```
[ your app + ] --multipart--> [ @aitofy/bugdeck-server ] --IssueTracker--> [ Plane ]
screenshot · annotation POST /reports SQLite + PNGs on disk issue + attachments
element pick · context your resolveUser() code back: PROJ-12
```
The store is the source of truth and the tracker is a mirror: filing happens **after** the user has
been answered, so a board that is slow or down never turns a filed bug into a 500. The create is
idempotent, so a retry adopts the issue instead of duplicating it.
### What the user gets
- **Capture the page** — or **one element**: a devtools-style picker highlights what is under the
cursor and photographs exactly that box.
- **Draw on it** — pen, box, arrow, and a real **crop** that cuts from the full-resolution image.
Undo covers the crop too.
- **Write with the pictures inline** — a block editor, so the order of "here is what I did, here is
what I saw" survives all the way to the issue.
- **Drafts that survive a refresh** — autosaved per page, restored with one line of notice. Closing
the tab keeps them.
- **Paste, drop or upload** images. Up to 10 per report, 10 MB each, re-encoded server-side.
- **Automatic context** — URL, viewport and pixel ratio, user agent, build commit, and the last API
failure the app saw before the report was filed.
- **Light and dark**, one accent colour prop, and a launcher in any of the four corners.
### What the operator gets
- **Self-hosted, SQLite by default.** One container, one volume. No database to run.
- **Your auth, not ours.** `resolveUser(request)` is a function you write; `null` is a 401.
- **Ownership, not tenancy.** Someone else's report is a 404 — a stranger has no business learning
that an id exists.
- **Images are re-encoded** before anything is stored: magic bytes, no SVG or GIF, 5000×5000
ceiling, `nosniff` on the way out.
- **Rate limited** to 10 writes per hour per user, in memory, no Redis.
- **Codes, never sentences.** A 4xx answers `{"error":"RATE_LIMITED"}` so the wording — and the
language — stays in the widget.
### Replying to the reporter
The server reads the tracker back every `POLL_INTERVAL` seconds and the issue's state follows the
report. A comment that starts with `@user` is carried down to the reporter as a reply in their own
thread; **every other comment stays internal**, so the board is still where your team argues. The
marker is `PUBLIC_REPLY_MARKER` and renaming it changes both halves at once.
### Shopify / WordPress
No app, no plugin, no marketplace listing. It is one snippet in your theme, next to the rest of
your scripts:
```html
```
A single-file `