Add a feedback widget to a SolidStart app

Add the widget script to the head of the document function in src/entry-server.tsx with defer, and read the public key from a VITE_ variable. Identify the signed-in user in a component that calls identify inside createEffect, so the call runs only in the browser. The script stays loaded across client-side route changes.

By 路 Published 路 Last updated

Where does the script go in the SolidStart document?

SolidStart renders the HTML shell in src/entry-server.tsx, inside a document function that receives the assets, children and scripts parts of the page. Put the widget script in the head of that function with defer. The server sends it in the first response, so the Feedback button is ready as soon as the page becomes interactive:

// src/entry-server.tsx
import { createHandler, StartServer } from "@solidjs/start/server";

export default createHandler(() => (
  <StartServer
    document={({ assets, children, scripts }) => (
      <html lang="en">
        <head>
          <meta charset="utf-8" />
          <meta name="viewport" content="width=device-width, initial-scale=1" />
          {assets}
          {import.meta.env.VITE_ESCUTA_KEY && (
            <script
              src="https://escutaproduto.com/widget.js"
              data-key={import.meta.env.VITE_ESCUTA_KEY}
              defer
            />
          )}
        </head>
        <body>
          <div id="app">{children}</div>
          {scripts}
        </body>
      </html>
    )}
  />
));

The {assets} slot holds your stylesheets and preloads, and {scripts} holds the hydration scripts that SolidStart needs, so keep both. The widget script sits beside them in the head. The && guard means no tag is rendered when the variable is empty, which avoids a button that cannot send during local work.

The data-key attribute is your public product key. Vite exposes only variables with the VITE_ prefix to code that runs in the browser, and import.meta.env reads them in this file too, so the key lands in the HTML as intended.

How do I keep the key in Vite environment variables?

Put the key in a .env file at the project root for local development, using the prefixed name:

VITE_ESCUTA_KEY=pk_your_dev_product_key

Set the same variable in your hosting provider for production, with the production product key. Keep the two products apart: create one Escuta Produto product for local work and another for the live site, and give each its own allowed origins. Add the address that vinxi dev prints, usually http://localhost:3000, to the development product. A request from an origin that is not on the list receives a 403, so a copied key cannot send from another site.

The key is public and ships in the HTML by design, and it can only create feedback. The widget reference lists the attributes you can add next to data-key.

Why must identify run only in the browser?

The server renders your components to HTML, and it has no window, no document and no visitor. Identify needs the widget queue on the page, and the signed-in user is known only where the visitor's session is read. Solid's createEffect does not run during server rendering, so code inside an effect runs only in the browser. That makes it the right place for the call.

The queue form also works before the widget loads. You push the identify call onto a list, and the widget processes it when it starts. This means the effect never needs to wait for the script tag.

How do I identify the user from route data?

Create a small component that takes the user as a prop and calls identify inside createEffect. Save it as src/components/FeedbackIdentity.tsx:

// src/components/FeedbackIdentity.tsx
import { createEffect } from "solid-js";

type FeedbackUser = {
  id: string;
  email: string;
  name?: string;
};

type EscutaWindow = Window & {
  EscutaProduto?: { q?: unknown[][] };
};

export default function FeedbackIdentity(props: { user?: FeedbackUser | null }) {
  createEffect(() => {
    const id = props.user?.id;
    const email = props.user?.email;
    const name = props.user?.name;
    if (!id || !email) return;

    const w = window as EscutaWindow;
    w.EscutaProduto = w.EscutaProduto || { q: [] };
    (w.EscutaProduto.q ||= []).push(["identify", [{ id, email, name }]]);
  });

  return null;
}

Read the values inside the effect, as shown, so Solid tracks them. The effect then runs again whenever the user signal changes, for example after a sign-in. Render the component in your root app, with the user from your authentication, which you load in a route's data function. A query that runs on the server, such as one that reads the session cookie, is the usual source. The component renders nothing, so it does not change the page layout.

If you only want a single call per page load, use onMount from solid-js in place of createEffect. For a signed-in app, the effect version keeps the identity current without a reload.

Does the widget keep running across client-side navigation?

Yes. SolidStart uses @solidjs/router, which swaps the page content in the browser when the visitor follows a link. The document, including the head and the widget script, stays in place. The Feedback button remains on screen, and the widget records the current address each time someone sends a message, so an item from /pricing and one from /account both point to the correct page.

If your root component renders FeedbackIdentity, it stays mounted during navigation too. Its effect re-runs only when the user changes, so moving between routes does not send duplicate identify calls. The widget has no sign-out call in its JavaScript API, so after the visitor signs out, reload the page to clear the identity.

How do I set a Content Security Policy in SolidStart?

The policy can be set in the server document as a meta tag, which avoids relying on middleware APIs that change between SolidStart releases. A meta tag supports script-src and connect-src, which are the directives the widget needs:

<meta
  http-equiv="Content-Security-Policy"
  content="default-src 'self'; script-src 'self' https://escutaproduto.com; connect-src 'self' https://escutaproduto.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:"
/>

Place the tag at the top of the head, before the widget script, so it applies to everything after it. Some directives, such as frame-ancestors, are ignored in a meta tag, so set those in a response header if you need them. SolidStart's streamed output can include small inline scripts for hydration, and a strict policy may block them, so check the browser console after a production build. The CSP guide for widgets explains how to find a blocked request.

How do I open the form from a Solid button?

To place your own button, add data-trigger="none" to the script tag in src/entry-server.tsx. That hides the floating button. Any element with a data-escuta-open attribute opens the form, and the value preselects the type. Valid values are bug, idea, praise and other:

<button type="button" data-escuta-open="bug">Report a bug</button>

The attribute works in JSX, and a button rendered by the router after the page loads also opens the form. Test it once in the browser, because the widget reads the attribute when the click happens. The widget reference lists every option.

Why might the widget be missing in a SolidStart build?

Start with the page source. If the widget.js tag is absent, the VITE_ESCUTA_KEY variable was empty when the server started, so check the .env file and restart the dev server. If the tag is there but the button does not appear, look at the console for a Content Security Policy error, which means the host is missing from script-src.

If sending fails with a 403, the origin of the page is missing from the allowed origins of that product. Add the exact address, including the port. If items lack a name, confirm that your route data returns the user during the browser session, and log the user prop inside the effect while you debug. The best practices guide covers where the button belongs on a page.

Checking the first SolidStart message in Escuta Produto

Run the dev server, sign in and open the Feedback button from one page. Move to a second page through a link and send another message. Both items should show the page they came from, and the first should show your name and email. Sign out, reload, and send a third message to confirm the anonymous path.

For the same identify pattern in a React app, the React guide covers the effect version with an auth context. The Vue guide shows the composable version of the same idea. Escuta Produto keeps the feedback from every product in one inbox, so a SolidStart app sits next to the rest of your work.

Frequently asked questions

Where do I add the feedback widget script in a SolidStart app?

In the head of the document function in src/entry-server.tsx, with the defer attribute. The server sends the script in the first response, so the Feedback button is ready as soon as the page loads.

How do I identify the signed-in user in SolidStart?

Render a component that takes the user as a prop and calls identify inside createEffect. Solid does not run effects during server rendering, so the call happens only in the browser and re-runs when the user changes.

Does the feedback widget keep working when I navigate between pages in SolidStart?

Yes. The router swaps page content in the browser without reloading the document, so the script and the Feedback button stay in place. The widget records the current address each time a message is sent.

Why do I need a VITE_ prefix for the feedback key in SolidStart?

Vite only exposes variables with the VITE_ prefix to code, so the document can read the key with import meta env. The product key is public by design, so it is safe to place in the page.