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
- 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. - Your backend signs a user token for the signed-in user with the private key and hands it to your frontend.
- The SDK sends it with every request as
Authorization: Bearer <user token>, next to the publishable key (X-Publishable-Key: pk_…). - NotificationStream checks the signature against the project's public keys and acts for
the user in the token's
subclaim.
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):
| Part | Field | Value |
|---|---|---|
| Header | alg | ES256. Other algorithms are rejected. |
| Header | kid | Optional. The signing key id (sig_…). Without it, each of the project's keys is tried. |
| Claims | sub | Required. Your user id, 1–255 characters. user_123 and User_123 are different users. |
| Claims | exp | Required. Expiry in epoch seconds, at most 7 days ahead. |
| Claims | nbf, iat | Optional. 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:
reason | Meaning |
|---|---|
malformed | Not a JWT, or sub / exp missing or invalid. |
algorithm | Not signed with ES256. |
unknown_key | The kid isn't one of the project's signing keys (for example, a deleted key). |
signature | The signature doesn't match any of the project's signing keys. |
expired | exp has passed. |
not_yet_valid | nbf is in the future. |
lifetime | exp 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:
- Create a second key on the API keys page.
- Deploy your backend signing with the new private key (and its key id, if you send
kid). - 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.