# 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 `