Feedtack

1. Deploy your own backend

About twenty minutes the first time. The backend is a Cloudflare Worker that you own and deploy. Nothing runs on our servers and nothing is billed to us: your comments, your database, your attachments, your email account.

What you need first

WhatCost
1A Cloudflare account. Sign up with an email, no card needed to start.Free tier
2An R2 subscription on that account. In Cloudflare's words: Storage & databases > R2 > Overview, then complete the checkout flow. It is a checkout, not a bill: the free allowance below still applies.Free tier
3A Resend account with a verified sending domain, and an API key. Resend is what delivers the notification emails.Free tier
4Node.js 18 or newer on your machine, to run Cloudflare's wrangler CLI. You do not install it globally: npx fetches it.Free

You can use any other transactional email provider, but you would have to change enviarCorreo() in worker/src/index.js. Out of the box it is Resend.

Get the code and log in

git clone https://github.com/alvaromassana/feedtack.git
cd feedtack/worker
npx wrangler login          # opens the browser and links the CLI to your account

Create the three pieces of storage

The Worker needs three Cloudflare resources. Create all three before deploying: wrangler deploy reads wrangler.toml, and a binding pointing at something that does not exist will stop the deploy.

# 1. The database, where the comments live
npx wrangler d1 create feedtack          # copy the database_id it prints

# 2. The bucket, where the screenshots live
npx wrangler r2 bucket create feedtack-adjuntos

The third one, the Durable Object that batches the notifications, needs no command: the migration already declared in wrangler.toml creates it on the first deploy.

Fill in wrangler.toml

Open worker/wrangler.toml and set:

FieldWhat to put
database_idThe id that d1 create printed.
DESTINOThe address the comments are emailed to, normally yours.
REMITENTEThe sender, on a domain you verified in Resend, for example Feedtack <feedtack@youragency.com>.
ORIGENES_PERMITIDOSComma-separated list of the sites allowed to send comments. Wildcards like *.pages.dev work. Anything else is blocked by CORS, on purpose.
BASE_PUBLICAYour Worker's own URL, so screenshots too big for the email get a download link. Leave it empty and the attachments simply travel without a link. You only know the URL after the first deploy, so fill this one in and deploy again.

VENTANA_MINUTOS and CORTE_COMENTARIOS control how comments are grouped into one email. The defaults are sensible; leave them alone until the volume tells you otherwise.

Create the tables, deploy, and set the secrets

npx wrangler d1 execute feedtack --remote --file=esquema.sql
npx wrangler deploy                                          # prints your Worker's URL

npx wrangler secret put RESEND_API_KEY                       # your Resend key
openssl rand -hex 24 | npx wrangler secret put CLAVE_ADMIN   # your team key, save it

Now put that Worker URL into BASE_PUBLICA in wrangler.toml and run npx wrangler deploy once more.

Check it is up

curl https://your-worker.workers.dev/salud      # {"ok":true}

If you get anything else, the usual suspects are a database_id that was not pasted in, the R2 bucket not created, or the schema not run against --remote.

What this costs you, in practice

A review tool moves a handful of comments a day, so the free tiers are not a constraint. Checked against the providers' own pricing pages on 19 September 2026:

Free allowanceLimit
Cloudflare Workers100,000 requests / day
Cloudflare D15 GB stored, 5 million rows read / day, 100,000 rows written / day
Cloudflare R210 GB stored / month, egress free
Resend3,000 emails / month, 100 / day

An agency with thirty client sites under review will not get near any of those. If you ever do outgrow them, you are the one who decides whether to pay, and to whom. We never see any of it.

2. Add the widget

Two ways in, pick one:

Your siteWhat to doWhy
WordPressInstall the plugin A settings screen instead of editing templates, and it refuses to load on a production site.
Anything elsePaste the tag, below Astro, Eleventy, Hugo, React, Next, Vue, plain HTML, Shopify, Squarespace, Wix.

The plugin and the tag load exactly the same widget and talk to the same Worker. Nothing else in this guide changes depending on which one you choose. On WordPress you can jump to the plugin and skip the rest of this section.

Paste the tag

🔴 There is no production guardrail here. The widget file has no idea which site it is on, so the tag loads whenever it is present. On a live site every visitor would see the button, write comments and read everyone else's. Removing the tag before you go live is on you. The WordPress plugin is the one that has the lock.

Host the file yourself. Copy widget/feedtack.js (103 KB, 28 KB gzipped, zero dependencies) next to your site's other assets and point the tag at your own copy:

<script src="/feedtack.js"
        data-site="client-slug"
        data-endpoint="https://your-worker.workers.dev"
        data-color="#4f46e5"
        defer></script>

This is the recommended way and it is what makes the install genuinely yours: your site stops depending on anyone else's repository or CDN staying up, and an update only happens when you decide to copy a new file.

If you would rather not host it, jsDelivr serves it straight from this repository:

<script src="https://cdn.jsdelivr.net/gh/alvaromassana/feedtack@main/widget/feedtack.js" ...

@main tracks this repository's main branch, so your clients' sites pick up our changes the moment we push them, including the broken ones. If you use the CDN, pin it to a release tag rather than @main, for example @v1.0.0 (releases).

AttributeWhat forDefault
data-siteIdentifies the client, shows up in the email subject.sin-identificar
data-endpointYour Worker's base URL.none, required
data-colorAccent colour, usually the client's brand.#4f46e5
data-labelButton text.Comentar / Comment
data-positionborde-derecho (a tab on the right edge), bottom-right, bottom-left, top-right.borde-derecho
data-langes or en, overrides the page's lang.auto

3. Hand out the links

  • Your team. Open the site once with ?feedtack_admin=YOUR_KEY. It is remembered in that browser. With the key you can resolve, mark your own notes as done, reopen and delete.
  • Each reviewer. Send them https://staging.example.com/?feedtack_yo=Their%20Name. From then on everything they write is signed, and they can edit or delete their own comments. Without a link, the widget asks for a name the first time and remembers it.

Without the key you can write, edit and delete your own, and confirm or reopen what has been resolved. Nothing else.

Sites renamed from Tack Comment keep working: the widget still accepts the old ?tack_yo= and ?tack_admin= links and still reads the identity saved in the browser under the old name.

The WordPress plugin

This is the recommended route on WordPress, and it replaces step 2: you do not paste any tag.

Download feedtack.zip and install it like any other plugin. The widget file travels inside the zip and is served from your own site, so the plugin has no external dependency. Settings live under Settings → Feedtack: site slug, Worker endpoint, colour, button text, position and panel language. Leave the language empty to follow the page's lang, and set it when the site is written in one language and reviewed in another.

The reason it exists is not compatibility, it is the lock: it will not load if wp_get_environment_type() returns production, unless you tick a box, which then shows a permanent red warning. It also stays out of the admin, ajax calls, cron and feeds. The environment comes from WP_ENVIRONMENT_TYPE in wp-config.php.

The plugin settings page, refusing to load on a production site

What lands in your inbox

Every new comment, edit, reopening and deletion sends an email with the text (previous version struck through on edits), the pointed elements with their selectors and sizes, the attachments, and the context: page, URL, viewport, browser, OS, scroll position and time.

The email you receive, with the comment, the CSS selector and the browser

The review workflow

open  ──(your team, with the key)──>  resolved  ──(the client)──>  confirmed
  ^                                       │
  └───────────(the client)──────────  reopened

The client writes and can edit their own while it is not closed. Your team resolves. The client confirms or reopens, and you get an email either way. Deleting, by the team or by the author, emails a copy, which is the only trace that is kept.

What is stored, and where

One table in your own D1 database, comentarios: the text, the pointed elements (selector, text, position), the page, the anonymous author id and name, the state, a history of state changes and previous texts, and the browser context. Replies live in their own table. The schema is worker/esquema.sql.

Attachments are emailed, not stored. The panel shows the count, not the files. Storing them would mean adding R2.

Honest limitations

  • Identity is not authentication. Who wrote what is an anonymous id in localStorage. Enough for a private staging site, not enough for anything sensitive. Switch browsers and you lose the ability to edit what you wrote: a personal link restores the name, not the id.
  • No rate limiting. An allowed domain can send as much as it likes.
  • Editing changes the text and the pointed elements, not the attachments.
  • Screenshots use getDisplayMedia, so the browser asks the reviewer to share their screen. On browsers without it they can attach a screenshot they took themselves.

The full picture is in SECURITY.md.

The guide for the people reviewing

Your client should not read any of this page. Send them the guide instead: Spanish or English, one short clip per action. There is also a narrated version. Same page for every client, no sign-up, no names inside.

It shows the interface, so it stops being true the day the interface changes. Regenerate it with node qa/guia-clips.mjs and deploy again.