Skip to content
Median
Esc
↑↓navigate↵open⌘Jpreview
On this page

Identity

Tie conversations to the signed in person, require sign in, and reset on sign out.

import { auth } from "@clerk/nextjs/server";
import { medianIdentity } from "@mediansh/agent-tools";

export const { GET } = medianIdentity(async () => (await auth()).userId);
<MedianSupport identity="/api/median/identity" />

Without identity, a conversation belongs to the browser. The widget keeps a random token in localStorage, so one person on two devices is two visitors with two histories. A signed user.id ties the conversation to the person on every device.

Median only believes an id that arrives with a hash your server signed.

Serve the identity route

medianIdentity builds a GET route that returns the median_pk_ id and the signed in person, already signed. It reads MEDIAN_KEY from the server environment, so the browser needs no Median value of its own.

Return an object to show your team more than an id:

import { medianIdentity } from "@mediansh/agent-tools";

export const { GET } = medianIdentity(async (request) => {
  const session = await getSession(request);
  if (!session) return null;

  return {
    id: session.user.id,
    name: session.user.name,
    email: session.user.email,
    avatarUrl: session.user.image,
    metadata: { plan: session.org.plan },
  };
});

medianIdentity(resolver, options) takes an optional key that replaces MEDIAN_KEY. signIn and apiUrl are for site sign-in.

Resolver return values

Returns Result
"user_123" That id, signed
{ id, name?, email?, avatarUrl?, metadata? } The id, signed, with the details your team sees
null or undefined Nobody is signed in. The visitor is anonymous
"", or an object whose id is empty or not a string Anonymous
Throws Anonymous. The server logs Median: the identity resolver threw.

Empty name and email values are dropped. An avatarUrl that is not https is dropped. An empty metadata object is dropped.

Response

Case Status Body
Signed in 200 { apiKey, user: { id, hash, name?, email?, avatarUrl?, metadata? } }
Nobody signed in, or the resolver threw 200 { apiKey }
No key 500 { error: { code: "missing_median_key", message } }
The key is not a Median key, or is malformed 500 { error: { code: "invalid_median_key", message } }

Every response carries cache-control: private, no-store, max-age=0 and vary: cookie, authorization.

The request the widget sends

Value
Method GET to the identity path
Credentials include, so your cookies are sent
Headers accept: application/json. No Authorization header
When Once when the component mounts, and again if the path changes
  • A session kept in a cookie works as is. A session kept only in a bearer token never reaches the resolver. Use Sign it yourself instead.
  • medianIdentity sets no CORS headers. Serve the route from the same origin as the page.
  • Components that mount together share one request. MedianSupport, MedianSupportModal, MedianFeedback, MedianContactForm and useMedianContact all take identity.
<MedianSupport identity="/api/median/identity" />
<MedianFeedback identity="/api/median/identity">
  <button>Share feedback</button>
</MedianFeedback>

Pass the key as well

<MedianSupport apiKey="median_pk_..." identity="/api/median/identity" />
Props Until the route answers If the route fails
identity only Nothing renders Nothing renders
apiKey and identity The launcher shows. The open panel shows a loading state Anonymous visitor

A route fails when the request errors or answers with a status outside 200 to 299. A resolver that returns null or throws is not a failure.

Sign it yourself

Use this when your session lives where the widget renders, or when your server cannot serve a Web Request route. Sign on the server with the same MEDIAN_KEY the widget id came from.

On the server:

import { signMedianUser } from "@mediansh/agent-tools";

app.get("/api/me", async (req, res) => {
  const user = req.session.user;
  if (!user) return res.json(null);

  res.set("cache-control", "private, no-store");
  res.json({
    id: user.id,
    name: user.name,
    email: user.email,
    medianHash: await signMedianUser(user.id),
  });
});

In the browser:

const me = useMe(); // your own hook that reads /api/me

<MedianSupport
  apiKey={import.meta.env.VITE_MEDIAN_PUBLIC_KEY}
  user={
    me
      ? { id: me.id, hash: me.medianHash, name: me.name, email: me.email }
      : undefined
  }
/>;
  • signMedianUser(userId, key?) reads MEDIAN_KEY when key is omitted. Call it on the server only.
  • VITE_MEDIAN_PUBLIC_KEY holds the median_pk_ id from publicKeyFromMedianKey(MEDIAN_KEY). The id is safe in client code.
  • Sign the exact string you pass as id. The check is character for character.
  • Any server that answers GET with { apiKey, user }, where user carries hash, also works as an identity route.

Site sign-in

The same route signs visitors in to your public site, so the site and the widget share one customer. Return an email, and say where your login page is:

export const { GET } = medianIdentity(resolveUser, {
  signIn: (returnTo) => `/login?next=${encodeURIComponent(returnTo)}`,
});

Then pick Your app under Site → Sign-in and paste the route’s URL. See Site sign-in.

Request Answer
No median_request parameter JSON for the widget, as above
?median_request=<id>, signed in A 302 to Median with a signed token
?median_request=<id>, signed out, with signIn A 302 to signIn(returnTo)
?median_request=<id>, signed out, no signIn A 302 back to the site, which says to sign in first
?median_request=<id>, no email 500 with Median: site sign-in needs the person's email.

Behavior

Situation Result
id and hash match The conversation follows the person on every device
Only one of id and hash is set Both are ignored. The visitor stays tied to the browser
hash does not match, or id is longer than 128 characters Anonymous, with a console error
The widget id and hash come from different keys Anonymous, with a console error
The browser already has anonymous conversations They move to the signed in person when the signed user arrives
The route’s user has no hash It is ignored
user prop and identity are both set The user prop wins. The route’s user is ignored
name, email, avatarUrl, metadata Shown to your team. Never verified
The key is revoked in Settings → API Its median_pk_ id stops working, and the widget hides itself

Nothing is stored until the visitor sends a first message. After that, a change of signed user syncs as it happens.

Require sign in

Use it for a product nobody uses signed out. Until somebody is signed in, the panel asks them to sign in, and no conversation is opened or read.

<MedianSupport
  identity="/api/median/identity"
  requireSignIn
  onSignIn={() => router.push("/login")}
/>

With a session you already have:

<MedianSupport
  apiKey="median_pk_..."
  user={session?.user}
  requireSignIn
  onSignIn={() => router.push("/login")}
/>
user The visitor gets
Any field set to a non-empty value The conversation
Missing or empty Sign in to chat and Ask Acme anything once you are signed in. No thread, no composer
  • Any field counts, not only id. A name or an email says somebody is there.
  • Sign in shows only with onSignIn. Without it, the panel shows the same text and no button.
  • With identity, the panel shows a loading state until the route answers, rather than inviting a signed in person to sign in.
  • Pass user as soon as your session resolves. The panel shows the sign in state until it arrives.
  • The launcher stays in the corner.
  • Nothing is read while the panel is locked. There is no thread and no unread count.

Reset on sign out

import { medianSupport } from "@mediansh/widget";

medianSupport.reset();

reset() removes this browser’s session token and saved email. The next message starts a new, anonymous visitor. The old conversations stay in your inbox.

reset() also signs out the user the widget holds at that moment.

You pass After reset()
user The old user is not sent again, even if your state still passes them. Once you pass someone else or nobody, or the widget unmounts, they can sign back in
identity The widget reads the route again. If the route still answers with the old user, that user is not sent

With user, call reset() before or after your own sign out. With identity, call it after, so the route already answers with nobody when it is read again.

A mounted widget reads the identity route when it mounts and when reset() runs. For a sign in that happens without a page load, give the widget a key that changes with the user, so it remounts and reads the route again.

Troubleshooting

Message Where Fix
Median: <MedianSupport> was given neither apiKey nor identity, so nothing is on the page. Pass the median_pk_ id from publicKeyFromMedianKey(MEDIAN_KEY), or the path of a route built with medianIdentity(). Browser console Pass apiKey or identity
Median: could not read the identity route. followed by /api/median/identity answered 500. It should return { apiKey, user } from medianIdentity(). Browser console Open the route in a browser tab and read the error in its body
Median: MEDIAN_KEY is missing. Add the key from Settings under API to your server environment, or pass { key } to medianIdentity. Route body, missing_median_key Set MEDIAN_KEY on the server
Median: MEDIAN_KEY must start with median_key_. Copy the Median key from Settings under API. Route body, invalid_median_key Use the median_key_ value, not the median_pk_ id
Median: MEDIAN_KEY is malformed. Copy it again from Settings under API. Route body, invalid_median_key Copy the whole key again
Median: the identity resolver threw. Server log Fix the resolver. Until then, every visitor is anonymous
Median: the user hash passed to <MedianSupport> did not match. Sign the same string you pass as `user.id` with the Median key that produced this widget id. Until it matches, this visitor gets a conversation of their own on every device. Browser console Sign the exact string you pass as id, with the key this widget id came from
Median: a user hash was passed to <MedianSupport>, but its widget id and hash came from different Median keys. Derive both from the same key. Browser console Derive apiKey and hash from one current MEDIAN_KEY
Median: MEDIAN_KEY is missing. Add the Median key from Settings under API to your server environment. Thrown by signMedianUser Set MEDIAN_KEY, or pass key
Symptom Cause
The same customer shows twice in your inbox They used two devices without a signed id
A signed in person’s conversations do not follow them to another device The resolver returned an empty id, or the route’s user had no hash
The sign in panel shows for a signed in person user is empty or arrives late
A new visitor sees the last person’s thread reset() was not called

Was this page helpful?