Install Merle
Two lines in your layout are enough. Everything after the first two sections is optional.
Add the feedback button
Sign in, make a workspace and add a product. Setup asks for your site's addresses, because Merle takes messages only from those sites. It then shows these lines with your product id filled in.
Put them once, just before </body>, in the layout every page shares, and deploy:
<script type="module" src="https://feedback.hellomerle.com/button.js"></script>
<merle-feedback product="your-product" endpoint="https://feedback.hellomerle.com" name="Your product"></merle-feedback>
In JSX, close the element (<merle-feedback … />) and add async to the script. In Astro, add is:inline to the script. Setup has versions for Next.js and for Vite with React.
With TypeScript, a build that renders the element in JSX fails with Property 'merle-feedback' does not exist on type 'JSX.IntrinsicElements' until you declare it. Add this as merle-feedback.d.ts, in a folder your tsconfig includes:
import 'react';
declare module 'react' {
namespace JSX {
interface IntrinsicElements {
'merle-feedback': HTMLAttributes<HTMLElement> & {
product: string;
endpoint?: string;
name?: string;
privacy?: string;
user?: string;
};
}
}
}
That is the whole install. Your backend doesn't change. Messages arrive without the customer's name until you do the sign-in step.
Or ask Claude Code to do it
Open Claude Code in your repo and paste this, with your own product id and name:
Add Merle's feedback button to this app. The instructions are at https://hellomerle.com/docs. My product id is your-product and customers know the app as Your product. Put the two lines in the layout every page shares. If that layout is a TypeScript JSX file, add the declaration the docs give. If the site sends a Content-Security-Policy, add the sources the docs list. Make no other changes. Run the build to check it still passes. Work on a new branch, show me the diff, and commit it on that branch.
Read the commit before you merge it. To add customers' names later, ask it to follow Who your customers are on this page. It must not write your identity secret into the repo.
If your site sends a Content-Security-Policy
Add these sources to the directives you already have:
script-src https://feedback.hellomerle.com;
connect-src https://feedback.hellomerle.com;
img-src data:;
media-src blob:;
The first two are all the button needs to open and send. Without img-src data:, Add a picture comes out blank. Without media-src blob:, Show me can't play a recording back before it's sent.
style-src needs nothing: no 'unsafe-inline' and no hash. With a strict style-src, a picture shows the page without its web fonts, and taking one logs a single blocked-style message in the console, which you can ignore. If you use a nonce with 'strict-dynamic', put the nonce on the script tag. The button's other files follow from it.
Settings on the element
| Attribute | What it does |
|---|---|
product | Your product id, from Setup. It isn't secret |
endpoint | Where messages go: https://feedback.hellomerle.com |
name | Your product's name as customers know it. The panel says "Message the Your product team" |
privacy | A https link to your own privacy policy. Without it, the panel links to Merle's |
user | A signed token for the customer who is signed in. See Who your customers are |
To blur something more in pictures and recordings, such as an account's email address on the page, add data-merle-mask to that element. Everywhere a person can type is always blurred.
From one address, Merle takes up to 10 messages, 5 voice messages, 5 pictures and 2 screen recordings a minute. What the feedback button records is written so you can link to it from your own privacy notice.
Who your customers are
This step is optional, and it's the only one that needs more than the two lines. The button works without it:
| Without this step | With it | |
|---|---|---|
| In your inbox | Messages have no name | You see the customer's name, and their company and plan if you send them |
| For the customer | They follow their messages on the browser they sent them from, and can ask for updates by email | They follow their messages on any device |
| What it takes | Nothing | A token on every page load: either your sign-in provider's, or one your server signs |
Merle never trusts a name the browser claims. Your sign-in has to vouch for the customer, with a token Merle can check. A token that doesn't verify is refused, and the message isn't filed as anonymous. Setup and the back office show when tokens are being refused, and why.
Clerk, Supabase, Auth0 or Firebase
No server code. Choose your provider in Setup, and pass the provider's token to the element as user. Merle finds the keys from the issuer. Each provider needs one thing set up first:
| Provider | First | Token on the page |
|---|---|---|
| Clerk | Add a JWT template named merle with "aud": "merle" | getToken({ template: 'merle' }) |
| Supabase | Turn on asymmetric JWT signing keys | session.access_token |
| Auth0 | Have an API identifier, which Auth0 puts in aud | The access token for that API |
| Firebase | Nothing. Setup asks for your project id | currentUser.getIdToken() |
Your own sign-in
This one needs server code. Merle gives you an identity secret in Setup, shown once. Set it on your server as MERLE_IDENTITY_SECRET, where your other secrets live, and add this function to the server code that knows who is signed in:
import { createHmac } from 'node:crypto';
export function merleUser(u: { id: string; name?: string; email?: string; company?: string; plan?: string }) {
const secret = process.env.MERLE_IDENTITY_SECRET;
if (!secret) return undefined;
const payload = Buffer.from(JSON.stringify({ ...u, exp: Math.floor(Date.now() / 1000) + 3600 })).toString(
'base64url',
);
return `${payload}.${createHmac('sha256', secret).update(payload).digest('base64url')}`;
}
Pass its result as <merle-feedback user="…"> on each page load. A token lasts an hour. On a static page, answer it from a small route and set it with document.querySelector('merle-feedback').setAttribute('user', token). Only id is required. Send only what you're happy for your team to see next to the customer's words.
Lost the secret? Make a new one in Settings. The old one keeps working for 24 hours while you deploy.
merle.yml
If your product is connected to a GitHub repo, you can keep its settings at the root of that repo. Merle reads the file from your default branch, and again on every push that changes it. The back office shows each setting as "from merle.yml". Without a repo, Setup and Settings cover where bugs go and your sign-in provider.
product: your-product # your product id
name: Your product # as customers know it
bugs: github # bugs straight to GitHub issues, or "you" to see them first
repo: your-org/your-repo # must be the repo this file is in
reply-time: Usually within a day # shown to customers after they send
linear: { team: ENG } # optional
identity:
provider: custom # custom, clerk, supabase, auth0, firebase or none
# issuer: … audience: … # for a provider
Check it works
After you deploy, open your site, click Feedback and send anything. Setup shows it the moment it arrives. If your site's address isn't listed, Merle refuses the message and Setup offers to allow that site.
Your team
You can use Merle alone. To add people, invite them from Settings by email, by GitHub login or with a link. People join only by invite, and access to a repo doesn't make someone a member. They sign in with an email link, Google or GitHub. Members decide, reply and write notes. Admins also manage products, secrets, members and the plan.
Stuck? Write to support@hellomerle.com.