> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Environments

> Separate development and production traffic with per-environment ShieldLabs domains and keys, so test data never mixes with live.

In ShieldLabs you separate development from production the same way you separate any other configuration: with **separate domains and separate key sets**, plus the matching snippet host.

The API shape is identical in every environment. A webhook from a development domain and a webhook from a production domain carry the same fields, the same `request_id` join key, and the same Risk Score (0-100) with its `signals`. Nothing about the integration changes between environments except the credentials and the host you load.

## Two things change per environment

<CardGroup cols={2}>
  <Card title="The domain and its key set" icon="key" href="/setup/domains">
    Register a separate domain for each environment (for example `dev.example.com` and `example.com`). Each domain gets its own public key, secret key, webhook endpoints, and request balance.
  </Card>

  <Card title="The snippet host" icon="globe">
    Production loads the snippet from `cdn.shieldlabs.ai`. Development loads it from `dev.cdn.shieldlabs.ai`. Same module, same exports, same call pattern.
  </Card>
</CardGroup>

## Snippet hosts

The only host that differs in your page code is the CDN that serves the module.

| Environment     | Snippet host                               |
| --------------- | ------------------------------------------ |
| **Production**  | `https://cdn.shieldlabs.ai/snippet.js`     |
| **Development** | `https://dev.cdn.shieldlabs.ai/snippet.js` |

Both serve the same ES module with the same `checkAnonymous`, `checkAuthenticatedUser`, and `forceCheck*` exports that [Install the snippet](/setup/snippet) covers for the full client setup.

<CodeGroup>
  ```html Production theme={null}
  <script type="module">
    const mod = await import('https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PROD_PUBLIC_KEY');
    mod.checkAnonymous();
  </script>
  ```

  ```html Development theme={null}
  <script type="module">
    const mod = await import('https://dev.cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_DEV_PUBLIC_KEY');
    mod.checkAnonymous();
  </script>
  ```
</CodeGroup>

## Use a separate domain and key set per environment

Register each environment as its own [domain](/setup/domains) in [app.shieldlabs.ai](https://app.shieldlabs.ai/). Every domain you add gets an independent [public key, secret key](/setup/keys), webhook endpoints, and request balance.

| Environment     | Registered domain | Snippet host            | Webhook endpoints           |
| --------------- | ----------------- | ----------------------- | --------------------------- |
| **Development** | `dev.example.com` | `dev.cdn.shieldlabs.ai` | your tunnel or staging URLs |
| **Production**  | `example.com`     | `cdn.shieldlabs.ai`     | your production handlers    |

Keeping them separate buys you:

* **Clean data.** Development traffic never lands in your production [dashboard](/features/traffic-analytics), so your visitor counts, [traffic sources](/features/traffic-analytics), and [patterns](/features/patterns) reflect real users only.
* **Isolated webhooks.** Each domain registers its own [webhook endpoints](/setup/webhooks), so test deliveries hit your local handler and never your production endpoint.
* **Independent balances.** Test runs draw down the development domain's request balance, not your paid production balance. Billing is [per request](/billing).
* **Blast-radius control.** A leaked development secret cannot call the [Server API](/api/server-api) for your production domain. Keys are scoped to a single domain. Webhook `whsec_…` secrets are scoped per endpoint.

<Warning>
  Do not reuse one key set across environments. The public key is bound to the domain it is served from (resolved from the `Origin`, `Referer`, or `Host`), so a production key on `dev.example.com` will be rejected with a `401`. And a single secret shared across environments means a development leak compromises production.
</Warning>

## Wire the host and keys through config

Load the host and public key from environment config instead of hardcoding them, so the same build runs in both environments. Keep the **Private API Key** (for [History API](/api/server-api) reads) and the **secret key** (for the Management API) server-side only, and store each webhook **`whsec_…`** for signature verification. None of them must reach the browser.

<CodeGroup>
  ```bash .env.development theme={null}
  SHIELDLABS_SNIPPET_HOST=https://dev.cdn.shieldlabs.ai
  SHIELDLABS_PUBLIC_KEY=YOUR_DEV_PUBLIC_KEY
  SHIELDLABS_DOMAIN=dev.example.com
  SHIELDLABS_PRIVATE_API_KEY=sec_YOUR_DEV_PRIVATE_API_KEY   # server-side only, History API reads
  SHIELDLABS_SECRET=YOUR_DEV_SECRET   # server-side only, Management API
  SHIELDLABS_WEBHOOK_SECRET=whsec_...   # server-side only, dev endpoint
  ```

  ```bash .env.production theme={null}
  SHIELDLABS_SNIPPET_HOST=https://cdn.shieldlabs.ai
  SHIELDLABS_PUBLIC_KEY=YOUR_PROD_PUBLIC_KEY
  SHIELDLABS_DOMAIN=example.com
  SHIELDLABS_PRIVATE_API_KEY=sec_YOUR_PROD_PRIVATE_API_KEY   # server-side only, History API reads
  SHIELDLABS_SECRET=YOUR_PROD_SECRET   # server-side only, Management API
  SHIELDLABS_WEBHOOK_SECRET=whsec_...   # server-side only, prod endpoint
  ```
</CodeGroup>

A framework component then reads the host and public key from config and loads the module the same way in every environment:

```jsx ShieldLabsTracker.jsx theme={null}
'use client';
import { useEffect } from 'react';

// Public values, safe to expose in the browser build.
const HOST = process.env.NEXT_PUBLIC_SHIELDLABS_SNIPPET_HOST;
const PUBLIC_KEY = process.env.NEXT_PUBLIC_SHIELDLABS_PUBLIC_KEY;

export function ShieldLabsTracker({ hashedUserId }) {
  useEffect(() => {
    let cancelled = false;
    (async () => {
      const mod = await import(`${HOST}/snippet.js?publicKey=${PUBLIC_KEY}`);
      if (cancelled) return;
      hashedUserId
        ? mod.checkAuthenticatedUser(hashedUserId)
        : mod.checkAnonymous();
    })();
    return () => { cancelled = true; };
  }, [hashedUserId]);

  return null;
}
```

<Note>
  Your strict [Content-Security-Policy](/setup/csp) must allow whichever snippet host that environment uses. A development build pointed at `dev.cdn.shieldlabs.ai` needs that origin in `script-src`, not `cdn.shieldlabs.ai`. Keep the jsDelivr dependency host `cdn.jsdelivr.net` in `script-src` in both environments, and the `connect-src` endpoints (`rest.shieldlabs.ai`, `webrtc.shieldlabs.ai`, `stun:ice.shieldlabs.ai:3478`) stay the same in both. A very strict `connect-src` may skip the local-network check; the main snapshot and the Risk Score are unaffected.
</Note>

## Receiving webhooks in development

Your development webhook needs a publicly reachable URL. Run a tunnel to your local server and register it as an endpoint on the development domain's **Webhooks** tab:

```bash theme={null}
# expose your local server
ngrok http 3000
# -> https://abc123.ngrok.app
```

Add `https://abc123.ngrok.app/webhooks/shieldlabs` as an endpoint in the [dashboard](https://app.shieldlabs.ai/), copy that endpoint's `whsec_…` secret into `SHIELDLABS_WEBHOOK_SECRET`, and verify `X-Shield-Signature` exactly as you will in production. The [Webhooks](/setup/webhooks) guide carries the verification logic, the single webhook per scored identification (the server waits up to \~60s for follow-up checks, then sends the final score once), and the at-most-once delivery caveat.

<Tip>
  Webhooks are at-most-once with no retries, so a tunnel that is down means a missed delivery. When your local handler is offline, read results back with the [History API](/api/server-api) using that domain's Private API Key. Make handlers idempotent on `request_id` either way.
</Tip>

## Test with real traffic before production

Risk Scores reflect the actual connection and browser environment of whoever loads the page, so the most useful test is **real traffic on a real development domain**, not synthetic requests.

<Steps>
  <Step title="Deploy the snippet on a development domain">
    Register `dev.example.com`, drop the `dev.cdn.shieldlabs.ai` snippet onto a staging or preview deployment, and let real visits flow through it.
  </Step>

  <Step title="Watch the data land">
    Confirm visits appear under your development domain in the [dashboard](/features/traffic-analytics) and that webhooks reach your tunnel. Check the `signals` array to see which [signals](/features/anonymity-signals) fired and why.
  </Step>

  <Step title="Tune your thresholds against the bands">
    Decide what your code does at each band (Clean 0-9, Low 10-29, Medium 30-59, High 60-100) and verify the behavior on this real traffic, the way [acting on the Risk Score](/guides/acting-on-risk-score) lays out.
  </Step>

  <Step title="Promote to production">
    Swap the host to `cdn.shieldlabs.ai` and the keys to the production domain's key set through config. Nothing else in your code changes.
  </Step>
</Steps>

<Note>
  A legitimate visitor can score high (a corporate proxy, a VPN, a privacy browser). Testing with real traffic on a development domain is the best way to see that distribution before you wire scores into a decision your application acts on. Decide on Score plus `signals` plus context, and tune thresholds gradually.
</Note>

## Promotion checklist

<AccordionGroup>
  <Accordion title="Snippet host">
    Switch the import URL from `dev.cdn.shieldlabs.ai` to `cdn.shieldlabs.ai`, or flip the `SHIELDLABS_SNIPPET_HOST` config value.
  </Accordion>

  <Accordion title="Public key">
    Use the production domain's public key in the snippet URL.
  </Accordion>

  <Accordion title="Webhook endpoints">
    Register production webhook endpoints in the dashboard **Webhooks** tab, not your development tunnel.
  </Accordion>

  <Accordion title="Secret key">
    Load the production Secret Key on your server for [Server API](/api/server-api) auth. It must never reach the browser.
  </Accordion>

  <Accordion title="Webhook signing secrets">
    Copy each production endpoint's `whsec_…` into your server environment for [webhook verification](/setup/webhooks).
  </Accordion>

  <Accordion title="CSP">
    Confirm your production [Content-Security-Policy](/setup/csp) allows `cdn.shieldlabs.ai` in `script-src`.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Public and Secret Keys" icon="key" href="/setup/keys">
    Where each key lives and why the secret never ships to the browser.
  </Card>

  <Card title="Domains" icon="globe" href="/setup/domains">
    Register a domain per environment and manage its webhook endpoints.
  </Card>

  <Card title="Webhooks" icon="signature" href="/setup/webhooks">
    Verify `X-Shield-Signature` and handle the single scored webhook per identification.
  </Card>

  <Card title="Acting on the Risk Score" icon="gauge" href="/guides/acting-on-risk-score">
    Turn the bands into allow, challenge, review, or block in your own code.
  </Card>
</CardGroup>
