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

# Node.js and server runtimes

> Create one shipdit client per process, wait for ready() at startup, and evaluate flags per request.

## Overview

In this guide you add `shipdit` to a Node.js service: one client per process, a `ready()` call at startup, and per-request evaluation context. Flag checks run in memory against the environment's snapshot, so they add no network call to a request.

This page uses `new-checkout` and `search-variant` from [Define flags in code](/config-as-code/define-flags), plus the `max-retries` number flag and `checkout-payload` JSON flag from [Create number and JSON flags](/flags/number-and-json). Add all four to `shipd.config.ts` and run `shipd push` so the generated `FlagDefinitions` includes them.

## Install

```bash theme={null}
pnpm add shipdit
```

`shipdit` requires Node.js 20 or later.

## Create a server SDK key

1. In the [dashboard](https://app.useshipd.com), open your project and select the environment.
2. Go to **SDK keys** and click **Create SDK key**.
3. Enter a name, set **Key type** to `server`, and create it.
4. Copy the key (`shipd_sdk_server_…`). It is shown once.

```bash .env theme={null}
SHIPD_SDK_KEY=shipd_sdk_server_…
```

Each SDK key belongs to one environment. See [Keys](/concepts/keys) for all credential types.

## Create the client module

Create the client once and import it everywhere. Pass a default for every flag. Defaults are served until the first snapshot arrives and whenever the edge can't be reached.

```ts src/lib/shipd.ts theme={null}
import { createClient } from "shipdit";
import type { FlagDefinitions } from "../generated/flags";

export const shipd = createClient<FlagDefinitions>({
  sdkKey: process.env.SHIPD_SDK_KEY!,
  stream: true,
  defaults: {
    "new-checkout": false,
    "search-variant": "control",
    "max-retries": 3,
    "checkout-payload": { mode: "strict", showPromo: true },
  },
});
```

`stream: true` enables realtime updates over WebSocket. Without it, the client polls every 30 seconds.

> [!NOTE]
>
> The SDK uses `globalThis.WebSocket`, which exists in Node.js 22 and later. On Node.js 20, pass a
> constructor from the [`ws`](https://www.npmjs.com/package/ws) package, or omit `stream`
> to poll only.
>
> ```ts theme={null}
> import WebSocket from "ws";
>
> createClient<FlagDefinitions>({ /* … */ WebSocket });
> ```

`createClient` throws if `sdkKey` is empty. The client connects to `https://edge.useshipd.com`, and `stream: true` adds the `https://ws.useshipd.com` gateway. See [`createClient`](/reference/sdk#createclient) for every option.

## Wait for ready at startup

Call `ready()` once before you accept traffic. It resolves when the first snapshot loads, or after `readyTimeoutMs` (5 seconds by default). It never rejects for network errors.

```ts src/server.ts theme={null}
import { serve } from "@hono/node-server";
import { app } from "./app";
import { shipd } from "./lib/shipd";

await shipd.ready();

export const server = serve({ fetch: app.fetch, port: 3000 });
```

## Evaluate per request

A server client handles many users at once, so pass the evaluation context on each call. On a server client, `identify()` only records the user's traits for audience insights. It never changes the context that later evaluations use.

```ts src/app.ts theme={null}
import { Hono } from "hono";
import { shipd } from "./lib/shipd";

export const app = new Hono();

app.get("/checkout", (c) => {
  const user = c.get("user"); // from your auth middleware
  const context = {
    userId: user.id,
    attributes: { plan: user.plan, country: user.country },
  };

  if (!shipd.isEnabled("new-checkout", context)) {
    return c.json({ flow: "legacy" });
  }

  return c.json({
    flow: "new",
    search: shipd.getVariant("search-variant", context), // "control" | "semantic" | "hybrid"
    retries: shipd.getNumber("max-retries", context),
    ui: shipd.getJson("checkout-payload", context),
  });
});
```

The same code works in Express:

```ts src/routes/checkout.ts theme={null}
router.get("/checkout", (req, res) => {
  const on = shipd.isEnabled("new-checkout", { userId: req.user.id });
  res.json({ flow: on ? "new" : "legacy" });
});
```

Without a `userId`, percentage rollouts can't assign a bucket. The server SDK doesn't infer location, so pass `country` or other traits yourself. See [Target users](/flags/targeting).

## Shut down cleanly

`close()` stops polling, closes the WebSocket, and flushes pending events. Await its promise before exiting; it resolves after delivery finishes, fails, or reaches the timeout (5 seconds by default).

```ts src/server.ts theme={null}
process.on("SIGTERM", async () => {
  // Keep recording while active requests drain; bound long-lived requests.
  await new Promise<void>((resolve) => {
    const deadline = setTimeout(() => {
      server.closeAllConnections();
      resolve();
    }, 5_000);
    server.close(() => {
      clearTimeout(deadline);
      resolve();
    });
  });
  await shipd.close();
});
```

## What happens on failure

Evaluation methods never throw.

| Situation | What the client serves |
| - | - |
| Before the first snapshot loads | `defaults` |
| Edge unreachable or timing out | Last good snapshot, else `defaults` |
| Flag key missing from the snapshot | `defaults[key]` |
| SDK key revoked (HTTP 401) | Last good snapshot; `onError` is called |

Pass `onError` to log background failures. For the full model, see [Local evaluation](/concepts/local-evaluation). For error messages, see [Errors](/reference/errors).

## Next

* [Identify users](/flags/identify-users)
* [Next.js](/sdks/nextjs)
* [Test flags locally and in unit tests](/sdks/testing)
* [SDK reference](/reference/sdk)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.