# Configuration

Configure the SDK **once** at app boot, then evaluate per user with
`new Client(user)`. `configure()` builds the process-wide machinery (HTTP +
blob cache + poll lifecycle) and registers your `attributes` transform.

## Server

```ts
import { configure, Client } from "@shipeasy/sdk/server";

configure({
  apiKey: process.env.SHIPEASY_SERVER_KEY!, // the SERVER key
  attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan, country: u.geo.country }),
});

// Per request:
const flags = new Client(req.user);
if (flags.getFlag("new_checkout")) { /* ... */ }
```

`configure()` kicks off a one-shot fetch so the first
`new Client(user).getFlag(...)` resolves against real rules with no extra
wiring. For a long-running server that should keep rules fresh, pass `poll: true`
and the SDK runs the background refresh for you:

```ts
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, poll: true });
```

## Browser

```ts
import { configure, Client } from "@shipeasy/sdk/client";

configure({
  clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY!, // the CLIENT key
  attributes: (u: MyUser) => ({ user_id: u.id, plan: u.plan }),
});

const flags = new Client(currentUser);
await flags.ready(); // optional — await the first /sdk/evaluate round-trip
flags.getFlag("new_checkout");
```

The browser is single-user: `new Client(user)` runs the transform and
`identify()`s the result, merging browser context (`locale`, `timezone`,
`path`, `referrer`, `screen_*`, `user_agent`) and a persisted `anonymous_id`.

## Fail-safe reads & the `logLevel` option

Every **runtime** method the SDK exposes — `getFlag`, `getFlagDetail`,
`getConfig`, `universe(name).assign()`, `getKillswitch`, `track`, and
`see()` — is guaranteed to **never throw** into your code. If anything goes wrong
internally (a bad `decode` callback, a malformed value, an unexpected state) the
call fails silently: it returns the documented safe default (`false` /
`undefined` / the not-enrolled result) and reports the swallowed error on
`console` so you still find out. A feature-flag read can never take down a
request.

`logLevel` controls how loud that reporting is:

```ts
configure({
  apiKey: process.env.SHIPEASY_SERVER_KEY!,
  logLevel: "warn", // default — "silent" | "error" | "warn" | "info" | "debug"
});
```

Ordering is `silent < error < warn < info < debug`; a level prints everything at
or below it. The default `"warn"` prints `error` + `warn` and stays quiet
otherwise. Pass `"silent"` to mute the SDK entirely. The same option exists on
the browser `configure({ clientKey, logLevel })` and on the SSR
`shipeasy({ serverKey, logLevel })` helper.

When one of these last-resort guards catches an internal SDK failure — a bug on
*our* side, not yours — the SDK also reports it to **Shipeasy's own** project so
we can find and fix SDK bugs across the apps that run it. This never touches
your project or your Errors tab, carries no user/app data beyond the error
itself, and is fire-and-forget (it can never slow down or break a read). It is
on by default; opt out with `disableInternalErrorReporting: true` on any of the
`configure({ apiKey })`, browser `configure({ clientKey })`, or
`shipeasy({ serverKey })` entry points.

> Setup is deliberately still loud: constructing `new Client(user)` before
> `configure()`, or loading a bad offline snapshot, throws — those are boot-time
> misconfigurations you want to see immediately, not per-request runtime reads.

## The `attributes` transform

`attributes` maps **your** user object into the Shipeasy attribute bag that
every flag / experiment evaluation sees. It runs once per `new Client(user)`.

```ts
type AttributesFn<U> = (user: U) => User; // User = { user_id?, anonymous_id?, ...targeting }
```

When you **omit** `attributes`, the transform is the **identity** function — the
object you pass to `new Client(...)` is used verbatim, so it must already be the
attribute bag (`{ user_id, anonymous_id, ...targeting }`).

## Identity / bucketing unit

Bucketing hashes on `user_id` (falling back to `anonymous_id`). To bucket on a
different attribute (e.g. `company_id`), the experiment carries a `bucketBy` —
make sure your `attributes` transform surfaces that attribute. See
[Advanced](./advanced.md).

## Network & telemetry defaults (environment-derived)

The SDK is **quiet by default outside production** — an app that embeds it never
phones home from a local dev machine or a CI run. Two switches control egress,
and both **default to ON in production and OFF everywhere else**:

| Option | Controls | Default |
| --- | --- | --- |
| `isNetworkEnabled` | **Any** outbound request — flag/experiment fetches, `track()`, exposure logging, `see()` reports, usage telemetry, internal error self-monitoring. When `false` the SDK is fully offline: reads return code defaults / overrides. | `true` in prod, `false` otherwise |
| `disableTelemetry` | Just the per-evaluation usage telemetry beacon ("tracking"/outside logging). | telemetry ON in prod, OFF otherwise |

Production is inferred, in order:

1. `SHIPEASY_ENV`, then `NODE_ENV` — a value of `production`/`prod` ⇒ production.
2. When neither is set (e.g. a Cloudflare Worker, or the browser, where there is
   no native `NODE_ENV`), the SDK's own `env` option is used — and it defaults to
   `"prod"`. So a real production deploy stays **on** by default; set `env: "dev"`
   (or pass the switch explicitly) to keep a non-standard build quiet.

Pass either option explicitly to override the environment default:

```ts
// Force the SDK fully offline regardless of environment (no requests at all):
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, isNetworkEnabled: false });

// Keep flag fetching but never emit usage telemetry:
configure({ apiKey: process.env.SHIPEASY_SERVER_KEY!, disableTelemetry: true });
```

Both options exist identically on the browser `configure({ clientKey, … })` and
the SSR `shipeasy({ serverKey, … })` entry points. `isNetworkEnabled: false` is
the production-safe equivalent of the test/offline modes below.

## Test & offline configuration

For tests, swap `configure()` for `configureForTesting()` (no network, seed
overrides) or `configureForOffline()` (evaluate real rules from a captured
snapshot). Both replace the active configuration and are read through the same
`new Client(user)` — see [Testing](./testing.md).

## SSR bootstrap (flags on first paint)

Server-render evaluated flags / configs / experiments so the browser SDK reads
them **synchronously on first paint** — no flash, no extra round-trip. The
`shipeasy()` server handle emits two declarative `<script>` tags. **No SDK key
is embedded** in the bootstrap tag.

```tsx
// app/layout.tsx — Next.js root layout (React Server Component)
import { shipeasy } from "@shipeasy/sdk/server";

export default async function RootLayout({ children }) {
  // Every tag value is configured once, here — the emit calls take no arguments.
  const se = await shipeasy({
    serverKey: process.env.SHIPEASY_SERVER_KEY ?? "",
    clientKey: process.env.NEXT_PUBLIC_SHIPEASY_CLIENT_KEY, // PUBLIC key, for the tags
    projectId: process.env.NEXT_PUBLIC_SHIPEASY_PROJECT_ID, // for the devtools tag
  });
  const boot = se.getBootstrapData();
  return (
    <html>
      <body>
        {/* Render REAL <script> elements — dangerouslySetInnerHTML scripts do NOT run. */}
        <script src={boot.bootstrap.src} {...boot.bootstrap.attrs} />
        {boot.i18nLoader && <script src={boot.i18nLoader.src} {...boot.i18nLoader.attrs} />}
        {children}
      </body>
    </html>
  );
}
```

For non-React SSR (Express, raw templates), `se.getBootstrapTags()` returns the
same two tags as an HTML string. See [i18n](./i18n.md) for the loader details.

### Every emitted tag reads the config

`clientKey`, `projectId`, `cdnBaseUrl` and `i18nDefaultProfile` on `shipeasy()`
are the defaults every tag carries, so the emit calls take no arguments. Pass an
`emit` option only to override one tag:

| Handle method | Defaults from `shipeasy()` |
| --- | --- |
| `se.getBootstrapData()` / `se.getBootstrapTags()` | `clientKey`, `i18nDefaultProfile`, `cdnBaseUrl` (plus this request's anon id + identity) |
| `se.getDevtoolsData()` / `se.getDevtoolsTag()` | `projectId`, `clientKey`, `cdnBaseUrl` |

### Devtools overlay tag

`se.getDevtoolsTag()` emits the hosted devtools overlay bundle
(`se-devtools.js`) — nothing to install, no overlay code in your bundle. It reads
the project id and public client key off the tag and opens with **Shift+Alt+S**
or on any page loaded with `?se=1`. It is `defer`red unless you pass
`{ defer: false }`: a developer tool never belongs on the critical rendering
path.

```tsx
const dev = se.getDevtoolsData();
<script src={dev.src} {...dev.attrs} />
```

Adding it unconditionally is fine: the overlay only opens for someone with a
signed-in Shipeasy session, so on a page where nobody has authenticated it
renders nothing and says nothing. Gating it on your own staff or environment
check is **optional** — worth it only if you'd rather the bundle not load for
end users at all:

```tsx
{isStaff && <script src={dev.src} {...dev.attrs} />}
```

The standalone `getDevtoolsTag(opts)` / `getDevtoolsData(opts)` exports build the
same tag without a request handle. See [Browser devtools](./browser-devtools.md).

## Server identity (no anon→identified flip)

By default the SSR bootstrap above evaluates **anonymously** — the root layout
has no user, so the browser SDK seeds anonymous flags and then flips when its own
`identify()` resolves. Register a **server identity resolver** and every server
evaluation (nav reads **and** the bootstrap tag) runs against the identified
user, so client and server agree and there is no flip. Identity comes from your
own session — the authoritative, unspoofable source.

Register it **once, outside your render path** (Next `instrumentation.ts`, a
server bootstrap module). The SDK calls it per request; read your session inside:

```ts
// instrumentation.ts — runs once at server startup; the resolver runs per request
import { setServerIdentity } from "@shipeasy/sdk/server";
import { auth } from "@/auth";

export async function register() {
  setServerIdentity(async () => {
    const session = await auth(); // reads this request's session cookie
    return session?.user?.email
      ? { user_id: session.user.email, email: session.user.email }
      : null; // null ⇒ anonymous request
  });
}
```

`shipeasy()` in your layout needs **no change** — it picks up the registered
resolver automatically. Precedence, highest first: an explicit `user` passed to
`shipeasy({ user })` › the `identify` option passed to `shipeasy({ identify })`
(per-call) › the registered resolver › the signed `__se_id` cookie ›
`__se_anon_id`. A resolver that returns `null` or throws leaves the request
anonymous — it never breaks the render. Flags are UX, never authorization: the
client can only be more restrictive than the server, never grant itself a flag
the server denied.

The identified user rides the bootstrap tag (`data-user`), so the **browser SDK
adopts it** on first paint — no `identify()` call needed for the flags to be
this user's from the start. If the browser also calls `flags.identify()`, it
**reconciles idempotently**: a call matching the server's identity is a no-op (no
extra `/sdk/evaluate`, no flip); a genuine change (new attributes or a different
`user_id`) re-evaluates. `data-user` carries the user's traits (PII) — anonymous
requests emit none.

## Environment variables (convention)

| Variable | Side | Purpose |
| --- | --- | --- |
| `SHIPEASY_SERVER_KEY` | server | server key for `configure({ apiKey })` / `shipeasy({ serverKey })` |
| `NEXT_PUBLIC_SHIPEASY_CLIENT_KEY` | browser | public client key for `configure({ clientKey })` |
