# Install the Fielded support chat widget

Fielded is a lightweight support chat widget. Add it to any web page with a
single `<script>` tag.

## TL;DR — paste this before `</body>`

```html
<script src="https://fielded.net/widget/loader.js?key=FIELDED_API_KEY" defer></script>
```

**Replace `FIELDED_API_KEY`** with your app's API key. Sign up at
`https://fielded.net/register` to get one, or — if you already have an
account — visit `https://fielded.net/agents/installation` and copy the
key shown there.

## Where to put it (per framework)

### Plain HTML / static site / WordPress / Webflow / Shopify
Open the file that contains your closing `</body>` tag (often `index.html`,
the theme's `footer.php`, or the site's "custom code" injection slot). Paste
the snippet right before `</body>`. No build step needed.

### Ruby on Rails
Edit `app/views/layouts/application.html.erb`. Paste the snippet right before
`</body>`. To gate it to certain pages, move it to a more specific layout
instead.

### React (Vite or Create React App)
Edit `index.html` (Vite) or `public/index.html` (CRA). Paste the snippet
before `</body>`. No component changes needed.

For a React-component-based install (e.g. only on certain routes), use
`useEffect` to inject the script:

```jsx
import { useEffect } from "react";
useEffect(() => {
  const s = document.createElement("script");
  s.src = "https://fielded.net/widget/loader.js?key=FIELDED_API_KEY";
  s.defer = true;
  document.body.appendChild(s);
}, []);
```

### Next.js
In `app/layout.tsx` (App Router) use the built-in `<Script>` component:

```tsx
import Script from "next/script";
<Script src="https://fielded.net/widget/loader.js?key=FIELDED_API_KEY" strategy="afterInteractive" />
```

In Pages Router, do the same in `pages/_app.tsx` or `pages/_document.tsx`.

### Vue / Nuxt / SvelteKit / Astro
Add the snippet to the framework's root HTML template (e.g.
`app.vue`, `app.html`, `src/app.html`, or your Astro layout) before `</body>`.

## Verifying it works

1. Restart your dev server if needed.
2. Reload any page in your app.
3. Look for the chat bubble in the bottom-right corner. Click it.
4. If you see the chat panel open, you're done.

If the bubble doesn't appear:
- Check the browser devtools network tab — the request to
  `/widget/loader.js?key=…` should return 200.
- Make sure the API key in the URL is correct (no extra spaces or quotes).
- Check your domain allowlist in Fielded settings — by default any domain is
  allowed, but if you've locked it down, your origin must be on the list.

## FAQ

### Is it safe to expose my API key in the page source?

Yes — it's designed to. The key in the loader URL is a *publishable*
identifier, the same kind of token Intercom calls an `app_id`, Stripe calls a
`pk_live_…` key, or Segment calls a "write key." It tells Fielded which app
is loading the widget, nothing more.

It does **not** grant access to read conversations, list customers,
impersonate agents, or change settings. Anyone who visits your site can
already see it in their browser devtools the moment the widget loads.

### Then what *is* secret?

Two things, both server-side only:

- **Your HMAC secret** — used to sign a signed-in user's identity so the
  widget trusts "this really is alice@acme.com." If it leaks, someone could
  impersonate your users in chat. Find it on your `/agents/installation`
  page after signing in.
- **Agent passwords and session cookies** — same as any web app.

### What if someone copies my key and embeds the widget on their own site?

They'd mostly be starting anonymous conversations against your inbox —
annoying, not catastrophic. The right defense is the **domain allowlist** on
your app's settings page (which restricts which origins the widget will boot
on), not key secrecy.

### Why do the snippets reference `ENV["FIELDED_API_KEY"]` if it's public?

Pure ergonomics, not secrecy. Putting it in an env var lets you point staging
at one app and production at another without touching your layout file.

### What does the widget actually load?

A small JavaScript file (under 30 KB) that injects the chat bubble and
connects to Fielded over WebSockets when opened. It does not load
third-party trackers, doesn't read other cookies on your site, and only
sends data the visitor types into the chat.

## Identity verification (optional, for signed-in users)

If your app already knows who the visitor is, you can pass their identity to
Fielded so they aren't asked for it again — they'll see a "Chatting as: …"
badge instead of a name/email form, and conversations land in your inbox
already tagged with the right user.

This requires two things:

1. Your app's **HMAC secret** — find it on
   `https://fielded.net/agents/installation` (after signing in) under
   "Identity Verification". Treat it like a database password: server-side
   only, never shipped to the browser.
2. A server endpoint that computes `HMAC-SHA256(secret, user.email)` and
   returns it to your frontend, which passes it to the widget.

### Pattern: Node / Express backend + React (Vite, CRA, Replit, etc.) frontend

This is the most common SPA setup. The Express server holds the secret and
signs the user's email; the React frontend fetches the signature and boots
the widget with it.

> **Important — replace the script tag, don't add to it.** On SPA pages, use
> **only** the `FieldedWidget` component below. Do **not** also include the
> generic `<script src="…/widget/loader.js…">` tag from the top of this doc.
> The loader processes any queued `Fielded("boot", …)` calls only during
> initial load; if the anonymous loader runs first, a later boot call from
> your component will be ignored and the user will appear unidentified.
>
> **Same-origin or proxy required.** The `fetch("/api/fielded/boot")` call
> uses `credentials: "include"` and assumes the React app and Express API
> are served from the same origin (typical Replit / single-server setup) or
> via a Vite/webpack dev proxy. If your frontend and backend are on
> different origins, configure CORS on Express with
> `cors({ origin: "https://your-frontend", credentials: true })` and use the
> full backend URL in the fetch call.

**Step 1 — Add two secrets to your environment**

```
FIELDED_API_KEY      = (your public API key from the page above)
FIELDED_HMAC_SECRET  = (your HMAC secret — server-side only)
```

**Step 2 — Express route** (e.g. `server/routes/fielded.ts`)

```ts
import { Router } from "express";
import { createHmac } from "crypto";

const router = Router();

router.get("/api/fielded/boot", (req, res) => {
  const user = req.user as
    { name?: string; email?: string; company?: string } | undefined;

  // Not signed in — boot anonymously, the widget will ask for name/email
  if (!user?.email) {
    return res.json({
      apiKey: process.env.FIELDED_API_KEY,
      identified: false,
    });
  }

  const secret = process.env.FIELDED_HMAC_SECRET;
  if (!secret) {
    console.error("FIELDED_HMAC_SECRET not set");
    return res.json({
      apiKey: process.env.FIELDED_API_KEY,
      identified: false,
    });
  }

  const userHmac = createHmac("sha256", secret)
    .update(user.email)
    .digest("hex");

  // ── Map your user record to Fielded's expected shape. ─────────────
  // Adjust the right-hand side of each line to match YOUR schema. Common
  // gotcha: many apps store firstName/lastName separately and have no
  // company field at all — sending empty strings makes the chat show a
  // blank identity badge.
  const name =
    (user as any).name ||
    [(user as any).firstName, (user as any).lastName].filter(Boolean).join(" ").trim() ||
    (user as any).fullName ||
    (user as any).displayName ||
    user.email.split("@")[0]; // last-resort fallback

  const company =
    (user as any).company ||
    (user as any).organization ||
    (user as any).org ||
    (user as any).companyName ||
    "";

  res.json({
    apiKey: process.env.FIELDED_API_KEY,
    identified: true,
    user: { name, email: user.email, company },
    userHmac,
  });
});

export default router;
```

> **Field-mapping tip — the #1 source of bugs in this recipe.** Fielded
> expects `{ name, email, company }`. Most apps don't store data that way:
> Drizzle/Prisma schemas often have `firstName` + `lastName` and no company
> at all; NextAuth sessions have `name`; Clerk has `firstName/lastName`;
> some shops use `fullName` or `displayName`. **Open your user model and
> verify the field names** before testing. The snippet above tries the most
> common variants and falls back to the email local-part so the identity
> badge is never blank, but you should still customize the mapping to your
> schema for the best UX.

Mount it in your main server file:

```ts
import fieldedRouter from "./routes/fielded";
app.use(fieldedRouter);
```

**Step 3 — React component** (e.g. `client/src/components/FieldedWidget.tsx`)

```tsx
import { useEffect } from "react";

export function FieldedWidget() {
  useEffect(() => {
    let cancelled = false;

    fetch("/api/fielded/boot", { credentials: "include" })
      .then((r) => r.json())
      .then((cfg) => {
        if (cancelled) return;
        const w = window as any;
        w.Fielded = w.Fielded || function () {
          (w.Fielded.q = w.Fielded.q || []).push(arguments);
        };
        if (cfg.identified) {
          w.Fielded("boot", {
            apiKey:   cfg.apiKey,
            user:     cfg.user,
            userHmac: cfg.userHmac,
          });
        } else {
          w.Fielded("boot", { apiKey: cfg.apiKey });
        }
        const s = document.createElement("script");
        s.src = "https://fielded.net/widget/loader.js?key=FIELDED_API_KEY"; // ?key=… ensures the launcher button picks up your app's brand color
        s.async = true;
        document.body.appendChild(s);
      })
      .catch(() => { /* silently degrade — no widget */ });

    return () => { cancelled = true; };
  }, []);

  return null;
}
```

**Step 4 — Mount it in your root component** (`client/src/App.tsx`)

```tsx
import { FieldedWidget } from "@/components/FieldedWidget";

export default function App() {
  return (
    <>
      {/* ...your existing app... */}
      <FieldedWidget />
    </>
  );
}
```

**Verifying it worked**

1. Sign in to your app.
2. The chat bubble appears bottom-right.
3. Click it — instead of a name/email form, you see a **"Chatting as: …"**
   badge with the signed-in user's info.
4. Send a message — it lands in the Fielded inbox already tagged with that
   user.

If the badge doesn't appear, open devtools → Network → check the
`/api/fielded/boot` response. `identified` should be `true` and `userHmac`
should be a 64-character hex string. If `identified` is `false`, your
session middleware isn't populating `req.user`, or the user record's
email field is named something other than `email`.

### Other backends

The same pattern works for Rails, Django, Laravel, Go, etc. — the only
language-specific bit is computing the HMAC. In Ruby it's
`OpenSSL::HMAC.hexdigest("SHA256", secret, user.email)`; in Python it's
`hmac.new(secret.encode(), user.email.encode(), hashlib.sha256).hexdigest()`.
Sign in and visit `https://fielded.net/agents/installation` for more
language samples.

## Storing the API key safely

The API key is **public** — it's designed to ship to the browser. There's no
secret to leak in the loader URL. That said, the recommended pattern is to
store it in an env var so you can swap apps (dev vs. prod) without touching
the layout:

- Rails / Node / Python: read from `ENV["FIELDED_API_KEY"]`
- Next.js / Vite: prefix with `NEXT_PUBLIC_` / `VITE_` so it's exposed to the
  client at build time

## Links

- Sign up: https://fielded.net/register
- Agent dashboard: https://fielded.net/agents
- This doc as HTML: https://fielded.net/install
- This doc as markdown: https://fielded.net/install.md
