DEV Community

Cover image for I built a self-hosted alternative to Marker.io - here's how it works under the hood
NeosiaNexus
NeosiaNexus

Posted on

I built a self-hosted alternative to Marker.io - here's how it works under the hood

The problem

If you've ever freelanced, you know the feedback loop from hell.

Your client opens the staging site, takes a screenshot, draws a red circle in WhatsApp, and sends you: "the thing on the left is weird."

You reply: "Which page? Which element? What screen size?"

Three days of back-and-forth later, you finally understand they wanted 2px more padding on a button.

Tools like Marker.io and BugHerd solve this — clients annotate directly on the page. But they start at $39-42/month, your feedback data lives on their servers, and annotations are screenshot-based, meaning they break when the layout changes.

I wanted something self-hosted, open-source, and smarter about how it anchors annotations. So I built SitePing.

What is SitePing?

A drop-in feedback widget (~23KB gzipped) that your clients use to draw rectangles on the live site, categorize their feedback (bug, change request, question), and submit it. Everything goes to your own database.

Demo

Try the live demo — draw annotations right now, no signup.

Setup in 60 seconds

npm install @siteping/widget @siteping/adapter-prisma @siteping/cli
npx @siteping/cli init    # scaffolds Prisma schema + API route
npx prisma db push
Enter fullscreen mode Exit fullscreen mode

Then in your layout:

import { initSiteping } from '@siteping/widget'

const { destroy } = initSiteping({
  endpoint: '/api/siteping',
  projectName: 'my-project',
})
Enter fullscreen mode Exit fullscreen mode

That's it. Works with React, Next.js, Vue, Svelte, Astro, or vanilla HTML.

The interesting part: how annotations survive layout changes

Most feedback tools take a screenshot or pin coordinates in pixels. Resize the window, redeploy with new CSS, and the annotation points to the wrong place.

SitePing takes a different approach. For each annotation, it stores three anchors:

  1. CSS selector — generated by @medv/finder, which produces the shortest unique selector
  2. XPath — a structural path through the DOM
  3. Text snippet — surrounding text content of the element

When re-displaying an annotation, the resolver tries all three in order with fuzzy matching, inspired by Hypothesis. If the CSS selector fails (element was restructured), it falls back to XPath. If that fails too, it searches for the text snippet.

On top of that, rectangle positions are stored as percentages of the anchor element's bounding box, not as pixel coordinates. So if the element grows on a wider viewport, the annotation rectangle scales with it.

// Stored as:
{ xPct: 0.12, yPct: 0.34, wPct: 0.50, hPct: 0.20 }

// Not as:
{ x: 150, y: 200, w: 300, h: 100 }  // ← breaks on resize
Enter fullscreen mode Exit fullscreen mode

Shadow DOM isolation

The widget renders inside a closed Shadow DOM. This means:

  • Your site's CSS can't break the widget
  • The widget's CSS can't leak into your site
  • No class name conflicts, no !important wars

But there's a catch — if the widget's overlay lived inside the Shadow DOM, any parent with overflow: hidden would clip it. So the annotation overlay and markers live outside the Shadow DOM, in the main document. The widget manages both layers.

Architecture

packages/
├── @siteping/widget             # Browser widget (Shadow DOM)
├── @siteping/adapter-prisma     # Server-side Prisma handlers
├── @siteping/adapter-memory     # In-memory store (testing)
├── @siteping/adapter-localstorage  # Client-side store (demos)
├── @siteping/cli                # CLI scaffold tool
└── @siteping/core               # Shared types, schemas, errors
Enter fullscreen mode Exit fullscreen mode

All adapters implement a SitepingStore interface. A shared conformance test suite (22 tests) validates any adapter. Want to build a Drizzle adapter? Implement the interface, pass the conformance tests, done.

The widget bundle never includes Prisma or Zod. The adapter never includes DOM code. Each package is independently published and tree-shakeable.

What it looks like in practice

SitePing Marker.io / BugHerd
Hosting Self-hosted — your DB SaaS
Price Free, MIT license $39-42+/month
Annotations DOM-anchored (CSS + XPath + text) Screenshot / pin-based
Layout resilience Percentage-relative, survives responsive Breaks on layout shifts
Integration npm install + one function call Script tags + dashboards
Bundle size ~23KB gzipped N/A (external script)

Current state — honest take

This is v0.9.3, pre-1.0. The test suite is solid (780+ unit tests, 29 Playwright E2E tests across Chromium, Firefox, WebKit), CI runs on every push, and I use it on my own client projects.

But it's not battle-tested at scale. There are rough edges. The dashboard UI doesn't exist yet. Webhook notifications are on the roadmap but not built.

I'm sharing it now because I'd rather get real feedback early than polish in a vacuum.

What's on the roadmap

  • [ ] Drizzle ORM adapter
  • [ ] Dashboard UI for reviewing feedback
  • [ ] Discord/Slack webhook notifications
  • [ ] MutationObserver for SPA re-anchoring
  • [ ] Nuxt / Astro / SvelteKit integrations
  • [ ] Screenshot fallback when re-anchoring fails

I'd love your help

Try it — the live demo takes 5 seconds. Break things.

Give feedback — is the API intuitive? Is the UX clear for non-technical clients? What's missing?

Contribute — the contributing guide has a step-by-step for building new adapters. Good first issues are labeled on GitHub.


Thanks for reading. If something feels off, that's exactly what I need to hear.

Top comments (0)