Identity verification

Prove to NotificationStream which user the SDK is acting for, with a user token your backend signs with a project signing key.

The publishable key is public, so on its own it can't prove who the user is. Every client request therefore also carries a user token: a short-lived JWT that your backend signs for the signed-in user with one of the project's signing keys. Without a valid token, the API answers 401 unauthorized, so nobody can read another user's notifications by guessing user ids.

Identity verification is always on. There is no setting to turn it off.

How it works

  1. The dashboard creates a signing key with each new project, and you can add one on the API keys page. The key pair is generated in your browser; only the public key is uploaded. The private key (a PEM) is shown once; the key id (sig_…) stays listed on the page.
  2. Your backend signs a user token for the signed-in user with the private key and hands it to your frontend.
  3. The SDK sends it with every request as Authorization: Bearer <user token>, next to the publishable key (X-Publishable-Key: pk_…).
  4. NotificationStream checks the signature against the project's public keys and acts for the user in the token's sub claim.

NotificationStream stores only the public keys. They can check tokens but not create them, so even a leak on our side can't be used to impersonate your users.

The token

A user token is an ES256 JWT (ECDSA P-256 with SHA-256):

PartFieldValue
HeaderalgES256. Other algorithms are rejected.
HeaderkidOptional. The signing key id (sig_…). Without it, each of the project's keys is tried.
ClaimssubRequired. Your user id, 1–255 characters. user_123 and User_123 are different users.
ClaimsexpRequired. Expiry in epoch seconds, at most 7 days ahead.
Claimsnbf, iatOptional. nbf is honored.

The API allows 60 seconds of clock difference for exp and nbf. A user's first request creates the project user; there is nothing to register beforehand.

Keep tokens short-lived (the Node SDK defaults to one hour) and let the SDK fetch a new one when it needs it (see Pass it to the SDK).

Sign the token

Store the private key as a secret on your backend, for example as NOTIFICATIONSTREAM_SIGNING_KEY (the PEM, line breaks included). Never ship it to a browser or app.

import { createUserToken } from "@notificationstream/node";

const userToken = await createUserToken(process.env.NOTIFICATIONSTREAM_SIGNING_KEY!, user.id, {
  keyId: "sig_…", // optional, sent as `kid`
  expiresIn: 3600, // seconds, the default
});

Any JWT library with ES256 support works the same way. The private key is a PKCS#8 PEM (-----BEGIN PRIVATE KEY-----).

A typical setup is a small endpoint that returns a token for the signed-in user:

// app/api/notifications/token/route.ts (Next.js)
import { createUserToken } from "@notificationstream/node";
import { getSession } from "@/lib/auth";

export async function GET() {
  const session = await getSession();
  if (!session) return new Response("Unauthorized", { status: 401 });
  const token = await createUserToken(process.env.NOTIFICATIONSTREAM_SIGNING_KEY!, session.user.id);
  return new Response(token, { headers: { "Cache-Control": "no-store" } });
}

Pass it to the SDK

Give the SDK the user (with the same id you signed) and the token, either as a string or as a function that fetches a fresh one:

<NotificationProvider
  publishableKey="pk_…"
  user={{ id: user.id, name: user.name }}
  token={() => fetch("/api/notifications/token").then((r) => r.text())}
>

With a function, the SDK calls it on start, again shortly before the token expires and once after a 401. With a string, the feed stops updating once the token expires, until you call identify() with a new one, so use a function for pages and apps that stay open. If the token's sub differs from user.id, the SDK reports an unauthorized error. The exact option names are on each SDK page.

If you call the REST API directly, send both headers on every request:

curl https://api.notificationstream.com/v1/client/feed \
  -H "X-Publishable-Key: pk_…" \
  -H "Authorization: Bearer <user token>"

When a token is rejected

The API answers 401 unauthorized, and error.details.reason says which check failed:

reasonMeaning
malformedNot a JWT, or sub / exp missing or invalid.
algorithmNot signed with ES256.
unknown_keyThe kid isn't one of the project's signing keys (for example, a deleted key).
signatureThe signature doesn't match any of the project's signing keys.
expiredexp has passed.
not_yet_validnbf is in the future.
lifetimeexp is more than 7 days ahead.

A project without signing keys rejects every client request; the error message says so.

Rotating signing keys

A project can have two signing keys at a time, so you can rotate without downtime:

  1. Create a second key on the API keys page.
  2. Deploy your backend signing with the new private key (and its key id, if you send kid).
  3. Delete the old key once its Last used time stops moving (it's updated at most hourly).

Tokens signed with a deleted key stop working within a minute. If a private key leaks, create a new key, switch your backend to it and delete the leaked key right away.