> For AI agents: the documentation index is at https://sdk.nimbusprivacy.xyz/llms.txt, and every page is available as Markdown at its URL with .md appended.

# Install and set up (/getting-started/setup)

Your app needs four things before the SDK runs: the package, its files served from your site, two
response headers, and a gateway token on mainnet. This page sets up each one, then checks them.

## Install the package

```bash
pnpm add @umbra/nimbus-solana-web @solana/kit
```

Install `@solana/kit` beside it. Every SDK function takes mints and wallets as a kit `Address`,
which its `address` function makes from a base58 string.

The SDK runs in the browser only. It needs IndexedDB, Web Workers, Web Locks and WebAssembly
threads, which a server render does not have. Load it from code that runs in the browser.

## Serve the SDK's files

The package ships the browser files the SDK runs in its `assets/` folder:

- **`nimbus_primitives_wasm_bg.wasm`**, the WebAssembly the SDK's cryptography runs in.
- **`prover-worker.js`**, the worker script each proof runs in.
- **`prover/`**, the WebAssembly prover the worker builds proofs with.
- **`witness-graph/`**, the WebAssembly module that prepares each proof's inputs.

Your app serves these from its own site. A browser starts a module worker only from a script on
the page's own origin. No preset hosts them for you.

### Copy them on every install

`nimbus-solana-web copy-assets <folder>` copies every file into a folder you serve. Run it after
every install, so an upgraded SDK never meets old files:

```json title="package.json"
{
  "scripts": {
    "postinstall": "nimbus-solana-web copy-assets public/nimbus"
  }
}
```

You can run it before `dev` and `build` instead, as `nimbus-solana-web copy-assets public/nimbus
&& next build`. The command creates the folder and overwrites files of the same names. It leaves
other files alone.

The copied files are build output. Add the folder to `.gitignore`:

```text title=".gitignore"
/public/nimbus/
```

### Point the SDK at them

Pass the folder's URL as `assetsBaseUrl`. It must end with `/`, and it may be relative to the
page.

```ts
import { loadNimbusSdk } from "@umbra/nimbus-solana-web";

const sdk = await loadNimbusSdk(
  { cluster: "mainnet" },
  { assetsBaseUrl: "/nimbus/" },
);
```

The SDK fetches the WebAssembly from that folder while it loads. Each proof starts a worker from
`prover-worker.js`, and the worker fetches `prover/` and `witness-graph/` beside it.

### How the SDK checks them

The package records the SHA-256 of every file it ships. The SDK hashes each file before it runs
it, and refuses one that does not match:

- The main WebAssembly is hashed before it starts. Its functions are then tested against known
  answers.
- The worker script is fetched and hashed before a worker starts from it.
- The worker hashes each file it loads in turn.

An altered file throws a `NimbusSdkError` with reason `dependency-nonconforming`. The message names
the file and both hashes. The usual cause is files left over from an older SDK, so run
`copy-assets` again.

`sdk.settings.assetPinning` says whether this copy of the SDK checks at all. A packed package reads
`"pinned"`. It reads `"unpinned"` only when the SDK runs from its source, and then nothing is
checked.

```ts
if (sdk.settings.assetPinning !== "pinned") {
  throw new Error("This SDK build does not check its files.");
}
```

## Turn on cross-origin isolation

The prover shares memory between threads, which a browser allows only on a page served with these
two headers:

```text
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```

Send both with every page that loads the SDK. Sending them with every response is simplest.

**Next.js**

```ts
import type { NextConfig } from "next";

const crossOriginIsolation = [
  { key: "Cross-Origin-Opener-Policy", value: "same-origin" },
  { key: "Cross-Origin-Embedder-Policy", value: "require-corp" },
];

const config: NextConfig = {
  headers: async () => [{ source: "/:path*", headers: crossOriginIsolation }],
};

export default config;
```

This covers `next dev`, `next start` and a deployment on Vercel.

**Vite**

```ts
const crossOriginIsolation = {
  "Cross-Origin-Opener-Policy": "same-origin",
  "Cross-Origin-Embedder-Policy": "require-corp",
};

export default {
  server: { headers: crossOriginIsolation },
  preview: { headers: crossOriginIsolation },
};
```

This covers `vite` and `vite preview`. The host that serves your build needs the headers too.

**Vercel**

```json title="vercel.json"
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        { "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
        { "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
      ]
    }
  ]
}
```

Use this for a static build on Vercel, such as Vite's.

**Cloudflare**

```text title="public/_headers"
/*
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp
```

Cloudflare Pages reads `_headers` from the root of the folder it deploys.

To check, open your app and type this into the browser's console:

```ts
console.log(crossOriginIsolated); // true on an isolated page
```

If it prints `false`, check the headers on the page's own response in the network panel. Headers
on the SDK's files alone are not enough.

Isolation changes what the page may embed. An image or script from another origin loads only if
that origin allows it, with `Cross-Origin-Resource-Policy` or CORS. Serve token logos from your own
site. Requests the SDK makes with `fetch` work as before.

## The mainnet gateway token

On mainnet, Nimbus's services sit behind a gateway. It accepts a request only when it carries a
token issued for your app. Without one, it answers 401.

To get one, email [dev@umbraprivacy.com](mailto:dev@umbraprivacy.com) or reach out to any Umbra
team member.

The RPC, the indexer and the context service are not behind the gateway. So without a token, a
session still opens, syncs and reads balances. Registration and every payment fail with reason
`unauthorized`.

### Pass it to the session

`openNimbusSession` takes the token in its `gatewayToken` option. Pass a string, or a function that
returns one.

**A fixed token**

```ts
import { openNimbusSession } from "@umbra/nimbus-solana-web";

const opened = await openNimbusSession(
  sdk,
  { connectedWallet: signer },
  { gatewayToken: "your-gateway-token" },
);
```

The string must be an RFC 6750 bearer token. Anything else throws `misconfigured`, and the
message never repeats it.

**A function**

```ts
import { openNimbusSession } from "@umbra/nimbus-solana-web";

let cached: string | undefined;

async function fetchGatewayToken(): Promise<string> {
  if (cached === undefined) {
    const response = await fetch("/api/gateway-token");
    cached = await response.text();
  }
  return cached;
}

const opened = await openNimbusSession(
  sdk,
  { connectedWallet: signer },
  { gatewayToken: fetchGatewayToken },
);
```

The SDK calls it for every request to the gateway and keeps nothing. Cache the token yourself, as
above. If the function throws, that request fails `unauthorized`.

Use a function when your app loads the token at run time, for example from your own backend.
Either way, a visitor can read any token their browser holds.

### Where it is sent

The SDK sends the token as `Authorization: Bearer` to the gateway's origins only.
`sdk.settings.gatewayEndpoints` lists them, each with the name an error uses for it:

```ts
for (const { service, endpoint } of sdk.settings.gatewayEndpoints) {
  console.log(`${service}: ${endpoint}`);
}
```

The RPC, the indexer, the context service, your own site and the proving key host never receive
it. The SDK never stores it, logs it, or puts it in an error. `closeNimbusSession` drops it.

## Check it works

Load the SDK and print what it resolved. This is a quick test that the files and headers are
right:

```ts
import { isNimbusSdkError, loadNimbusSdk } from "@umbra/nimbus-solana-web";

try {
  const sdk = await loadNimbusSdk(
    { cluster: "mainnet" },
    { assetsBaseUrl: "/nimbus/" },
  );
  console.log(sdk.settings.assetPinning, sdk.settings.endpoints);
} catch (thrown) {
  if (isNimbusSdkError(thrown)) console.error(thrown.reason, thrown.message);
}
```

It can throw these:

| Reason                     | Cause                                                                                                      |
| -------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `unsupported-browser`      | The page is not cross-origin isolated, or the browser lacks a feature the SDK needs. The message names it. |
| `misconfigured`            | `assetsBaseUrl` does not end with `/`, or an endpoint is missing. The message names it.                    |
| `service-unavailable`      | The WebAssembly could not be fetched. Check that `copy-assets` ran.                                        |
| `dependency-nonconforming` | A served file is not the one this SDK ships. Run `copy-assets` again.                                      |

[Troubleshooting](https://sdk.nimbusprivacy.xyz/production/troubleshooting) covers each one in more depth.

## Next

- [Quickstart](https://sdk.nimbusprivacy.xyz/getting-started/quickstart): Deposit, pay and withdraw in one walkthrough.
- [Use it with React](https://sdk.nimbusprivacy.xyz/getting-started/react): Load once, follow wallets, keep the session in context.
- [Endpoints](https://sdk.nimbusprivacy.xyz/production/endpoints): The mainnet endpoints, your own RPC and your own services.


Last updated 21 October 2026
