# Install Jetti

Reference documentation for Jetti's client snippet and the API that mints a per-review install session. Each Jetti review captures DOM and behavior from a target web app via a small first-party `<script>` tag.

## What gets installed

A single `<script>` tag with `data-jetti-session` and `data-jetti-api` attributes, served from `https://www.jetti.co/snippet.js`. The snippet is part of the initial HTML response and runs on first paint — capture binds before the page becomes interactive.

## Where to install — per-stack recipes

The snippet belongs in the SSR/initial-HTML root template, immediately before `</head>`.

### JS frameworks

- Next.js (app router): `app/layout.tsx` — inside the rendered `<head>`
- Next.js (pages router): `pages/_document.tsx` — inside the `<Head>` of `<Html>`
- Nuxt 3: `nuxt.config.ts` `app.head.script` array, or `app.vue`
- Remix / React Router 7: `app/root.tsx` Document `<head>`
- Astro: root layout `<head>` (e.g. `src/layouts/Layout.astro`)
- SvelteKit: `src/app.html` (the `%sveltekit.head%` block) or root `+layout.svelte`
- Gatsby: `gatsby-ssr.js` `onRenderBody` `setHeadComponents`
- Expo (Expo Router web): `app/+html.tsx` — the snippet is a JSX `<script>` element inside `<head>`. The file is scaffolded via `npx expo customize app/+html.tsx` when not yet present. Expo only consumes `app/+html.tsx` or `public/index.html` for web HTML; `web/index.html` is not a valid Expo path.
- Expo (classic web, no Expo Router): `public/index.html` — scaffolded via `npx expo customize public/index.html` when not yet present.
- Create React App / Vue CLI / Angular: `public/index.html` or `src/index.html`
- Vite + plain SPA: `index.html`

### Server-rendered

- Rails: `app/views/layouts/application.html.erb`
- Django: `templates/base.html`
- Laravel: `resources/views/layouts/app.blade.php`
- Phoenix: `lib/*_web/components/layouts/root.html.heex`

### Static site generators

- Hugo: `layouts/_default/baseof.html` (or the active theme's baseof)
- Jekyll: `_includes/head.html` or `_layouts/default.html`
- Eleventy / 11ty: `_includes/layout.njk` (or whichever extends the root)

### Plain HTML / multi-page sites

- Every top-level `*.html`. When the site uses a templating step or shared partial, the partial is the canonical place.

### Tag manager

- GTM works via a Custom HTML tag firing on All Pages.

## Snippet requirements

- Framework head managers (`next/head`, `react-helmet`, `useHead`, `<svelte:head>`, Remix `Meta`, vue-meta) may de-dupe, reorder, or strip `data-*` attributes from third-party scripts. The snippet depends on its `data-jetti-session` and `data-jetti-api` attributes, so a raw `<script>` tag is required.
- The snippet runs on first paint to bind capture. A snippet placed inside `useEffect`, `onMount`, or a client-only component fires after capture is needed.
- One installation at the root layout covers every route. Per-page installs duplicate work and create attribution gaps.
- In a multi-app monorepo, the snippet belongs only in the app whose deployed origin matches the review's `targetUrl`.
- A site that sets a Content Security Policy via `<meta http-equiv>` or middleware needs `https://www.jetti.co` in `script-src` and `connect-src`.
- The Jetti install is scoped to the head insertion. Lockfile changes, reformatting, and surrounding refactors are out of scope.

## Conditional rendering (optional)

By default the snippet runs everywhere the `<script>` tag is installed. Two opt-in patterns gate it to specific environments — both are optional, and combining them is fine.

### Snippet-side host gating (no build changes)

The snippet reads two optional attributes from its own `<script>` tag. When either is set, the snippet mounts only on matching hosts; when both are absent, the snippet runs everywhere (current default).

- `data-jetti-hosts="staging.acme.com,*.preview.acme.com,localhost"` — allow-list. Snippet mounts only on hostnames that match one of these patterns.
- `data-jetti-disabled-hosts="acme.com,www.acme.com"` — block-list. Snippet bails on matching hostnames. Block-list takes precedence over allow-list.
- Pattern grammar: comma-separated; `*` matches one or more characters; matching is case-insensitive; the full hostname (`window.location.hostname`) must match end-to-end.
- On bailout the snippet logs `console.info('[Jetti] disabled on <hostname> …')` and returns before binding capture.

Example: install once, only run on staging and preview URLs.

```html
<script
  src="https://www.jetti.co/snippet.js"
  data-jetti-session="…"
  data-jetti-hosts="staging.acme.com,*.preview.acme.com,localhost">
</script>
```

### Framework-native env conditional (zero runtime cost)

For teams that prefer the snippet never even download on production, wrapping the `<script>` tag in the host framework's env conditional keeps the tag out of the rendered HTML entirely.

- Next.js: `{process.env.NEXT_PUBLIC_JETTI_ENABLED === 'true' && <Script ... />}` (the `NEXT_PUBLIC_` prefix is required for client-visible env vars).
- Vite: `{import.meta.env.MODE !== 'production' && <script ... />}` inside a JSX/TS component (not in raw `index.html`).
- Rails: `<%= render 'jetti' unless Rails.env.production? %>`.
- Django: wrap the include in `{% if not is_production %}…{% endif %}` with the template context populated per environment.
- Plain HTML / SSG: a build-step swap is the equivalent — emit the tag only in staging/preview build pipelines.

The two patterns compose: a privacy-strict install can wrap the tag in `process.env.X &&` and also set `data-jetti-disabled-hosts="acme.com"` as belt-and-suspenders.

## Session API

Jetti mints a per-review snippet via `POST /api/agent-installs` at https://www.jetti.co.

Request body:

```json
{
  "targetUrl": "https://example.com",
  "sessionName": "Example review",
  "creatorEmail": "developer@example.com"
}
```

Response includes `sessionId`, `snippetCode`, `reviewerLink`, `ownerLink`, `handoffLink`, `listenCommand`, and `verificationStatus`.

When a developer already has a snippet `<script>` tag from a prior session, that tag stands on its own — minting a new session is unnecessary.

## After install: arm the listener

After patching the snippet in, run the returned `listenCommand` (with `--auto` appended) so the next reviewer Send applies automatically:

```bash
npx jetti listen --session=<sessionId> --owner-token=<ownerToken> --api-origin=https://www.jetti.co --auto
```

The listener subscribes via Server-Sent Events to a session-scoped event stream and spawns `jetti apply <url>` as a child process on each Send. This closes the install-to-review loop in one command. Pass `--json` to receive JSONL events on stdout if you're wrapping the listener in another agent host (Claude Code, Codex, Cursor).

Claude Desktop users currently can't host the listener subprocess directly. The recommended V1 workaround is to open Terminal and run the `listenCommand` there. A follow-up MCP wrapper exposing `await_next_review(session_id)` is planned to bring Claude Desktop parity without changing the SSE contract.

## Authorization scope

Jetti is for sites the developer owns, controls, or is authorized to review. It is not a tool for bypassing logins, paywalls, CAPTCHAs, bot defenses, private systems, or third-party access controls.