Node.js SDK
@nopii/node is the server half of a protected app. It mints sessions for the browser, points your LLM client at the NoPII proxy, and tokenizes text your server handles directly.
Preview
Install
npm install @nopii/nodeimport { NoPIIServer } from "@nopii/node";
const nopii = new NoPIIServer({ secretKey: process.env.NOPII_SECRET_KEY! });Use a secret key (nsk_...) and keep it on the server. The constructor refuses a publishable key. One instance is safe to share across concurrent requests.
Mint client sessions
Add a route your frontend calls for a session. Bind it to your own user id: that is what lets the user reveal their earlier messages after a reload or on another device, and what keeps other users from revealing them.
// app/api/nopii-session/route.ts (Next.js)
export async function POST(req: Request) {
const user = await requireUser(req); // your auth
const session = await nopii.clientSessions.create({ endUserId: user.id, ttlSeconds: 900 });
return Response.json({ token: session.token, expiresAt: session.expiresAt });
}Sessions last 15 minutes by default and at most an hour. The browser asks again when one is about to expire.
Forward messages to the LLM
The browser sends your route tokens. Forward them through the proxy in tokens-only mode: the model is told what the tokens stand for, and the reply comes back tokenized for the browser to reveal.
Vercel AI SDK
import { createOpenAI } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json(); // already tokenized in the browser
const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
...nopii.proxySettings({ request: req, mode: "tokens-only" }),
});
const result = streamText({
model: openai("gpt-4o"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}proxySettings reads the X-NoPII-Session-Id header the browser SDK sent, so tokens the proxy creates for anything the browser missed stay revealable by the same user. Both openai(...), which uses the Responses API, and openai.chat(...) work. For Anthropic, spread the same settings into createAnthropic.
Official OpenAI and Anthropic clients
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
...nopii.openAIClientOptions({ request: req, mode: "tokens-only" }),
});
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
...nopii.anthropicClientOptions({ request: req, mode: "tokens-only" }),
});Use the helper that matches the client
/v1/messages itself, while OpenAI clients expect /v1 in the base URL. Mixing them up sends requests to /v1/v1/messages.Sessions without a request object
Deep inside a job or a helper, wrap the work in withSession. Every proxy call made through the SDK's fetch inside it, including in nested async code, carries that session. Concurrent requests keep their own.
const openai = new OpenAI({ apiKey, ...nopii.openAIClientOptions() });
await nopii.withSession(sessionId, () => summarize(openai, thread), "tokens-only");Tokenize on the server
For text that never passes through the proxy, such as documents you index for retrieval or records you import:
const result = await nopii.tokenize(["Contract signed by Wei Chen"], { endUserId: customer.id });
result.texts[0]; // "Contract signed by [NAME: ...]"
const { entities } = await nopii.detect(["Is there PII in here?"]); // not tokenized, not meteredPass endUserId for data that belongs to one of your users, so that user can reveal it later and no one else can.
Reveal grants for support tools
A secret key does not reveal another user's tokens by default. When a support agent needs to read a customer's conversation, create a short-lived grant and hand its id to the agent's browser.
const grant = await nopii.revealGrants.create({
subjectEndUserId: ticket.customerId,
granteeEndUserId: agent.id,
ttlSeconds: 900,
reason: `ticket ${ticket.id}`,
});
// In the agent's browser: nopii.reveal(text, { grantId: grant.id })
await nopii.revealGrants.revoke(grant.id); // when the ticket closesEvery reveal under a grant is logged. Your own server can use a grant with no grantee: nopii.detokenize(tokens, { grantId }).
Reference
| Member | Does |
|---|---|
clientSessions.create({ endUserId, ttlSeconds }) | Mints a session token for a browser or app |
proxySettings({ request, sessionId, mode }) | baseURL, headers, and fetch for AI SDK providers |
openAIClientOptions(...) | Options for new OpenAI |
anthropicClientOptions(...) | Options for new Anthropic |
withSession(sessionId, fn, mode) | Applies a session to proxy calls made inside fn |
tokenize(texts, { endUserId, sessionId }) | Detects and tokenizes |
detect(texts) | Detects only |
detokenize(tokens, { sessionId, grantId }) | Reveals under the reveal policy |
revealGrants.create(...) / revoke(id) | Manages reveal grants |
Related
- Browser SDK: the client half of Tier 2
- Keys and reveal policy: sessions, grants, and the reveal log
- Mock server: run the whole flow locally