ग्राहक · developer docs

Add Grahak to your product

Grahak is a support widget you drop into your app. Your users are already signed in with you, so you vouch for them: your backend mints a short-lived token, and the widget trades it for a session. You never build a chat UI or run a server for messages.

How it fits together

Two pieces of code get you live: an endpoint on your backend that returns a signed token for the logged-in user, and one call (or one provider) on your frontend that boots the widget.

Your backend

signs a token

Your frontend

boots the widget

Grahak

opens the session

1

Install the SDK

Plain JavaScript works in any web app, whatever your framework. If you build in React, the wrapper keeps the widget mounted for you. Pick one:

Terminal
npm install @grahak/browser
2

Mint an identity token

This is the only backend work. You sign a token for the logged-in user with an RSA key. Grahak stores only the public half, like an SSH key, and verifies every token against it.

Generate a keypair

Terminal
# Private key — keep this secret, on your server only
openssl genpkey -algorithm RSA -pkcs8 -out grahak-private.pem -pkeyopt rsa_keygen_bits:2048

# Public key — paste this into Console, Identity keys
openssl pkey -in grahak-private.pem -pubout -out grahak-public.pem

Add the public key

In Console, open your project, go to Identity keys and paste the contents of grahak-public.pem. Keep grahak-private.pem on your server as a secret, for example GRAHAK_PRIVATE_KEY. You can add and rotate multiple keys.

Sign a token for each user

The token carries a few claims. Everything else is optional display data.

aud
required
Your project ID.
sub
required
Your stable, immutable user ID (not an email). Becomes the customer's external ID.
exp
required
Expiry. Keep it short (Grahak rejects tokens older than 10 minutes).
email, name, avatar_url
optional
Display only. Refreshed on every load.
metadata
ignored
No longer read here. This token passes through the browser, where the customer can decode it, so your own data about them (plan, MRR, churn risk) is pushed server-to-server instead. Send flat values: strings, numbers and booleans render as themselves, while a nested object or array can only be shown as raw JSON.
route.ts
// GET /api/me/grahak-identity  (Next.js route handler)
import { SignJWT, importPKCS8 } from "jose";

const PROJECT_ID = process.env.GRAHAK_PROJECT_ID!;
const PRIVATE_KEY = process.env.GRAHAK_PRIVATE_KEY!; // PKCS#8 PEM

export async function GET() {
  const user = await getCurrentUser(); // your existing auth

  const key = await importPKCS8(PRIVATE_KEY, "RS256");
  const jwt = await new SignJWT({
    email: user.email,
    name: user.name,
    avatar_url: user.avatarUrl,
  })
    .setProtectedHeader({ alg: "RS256" })
    .setAudience(PROJECT_ID)   // aud = your project ID
    .setSubject(user.id)       // sub = your stable user ID
    .setIssuedAt()
    .setJti(crypto.randomUUID()) // unique per token, or two mints in the
    .setExpirationTime("5m")     // same second collide and the 2nd is refused
    .sign(key);

  return Response.json({ jwt });
}
Serve this behind your own auth, for example GET /api/me/grahak-identity returning { jwt }. Mint a fresh token each time the widget connects: tokens are single-use, so a replayed one is rejected.
3

Embed the widget

Point the widget at that endpoint and mount it. Pass userJwt as a getter, not a string, so each reconnect (a reload, a re-login) fetches a fresh token.

widget.ts
import { init } from "@grahak/browser";

const widget = init({
  projectId: "YOUR_PROJECT_ID",
  // A getter, not a string: tokens are single-use, so each
  // (re)connect fetches a fresh one.
  userJwt: async () => {
    const res = await fetch("/api/me/grahak-identity");
    const { jwt } = await res.json();
    return jwt;
  },
});

widget.mountOverlay(); // floating launcher, bottom-right
mountOverlay() adds a floating launcher in the corner and manages the panel for you. Want it inline instead of floating? See mode: "attach" in the reference.

Anonymous visitors

For logged-out pages, a docs site or a landing page, skip token minting and let visitors talk to you anonymously.

widget.ts
import { init } from "@grahak/browser";

// No token: visitors talk to you anonymously.
init({ projectId: "YOUR_PROJECT_ID", anonymous: true }).mountOverlay();

Running both? Pass userJwt and anonymous together: signed-in visitors are identified, everyone else stays anonymous. Grahak remembers an anonymous visitor in their browser, so a returning one keeps the same thread.

Your data on a customer

Push what your support team should know about a customer, their plan, seat count, MRR, renewal date, churn risk, and it appears beside every thread they open.

Not in the token. The identity JWT passes through the browser, and a JWT is base64, not encrypted, so the customer can read every claim in it. Anything you would not show them, and that is most of what is useful here, has to travel backend to backend. A metadata claim on the identity token is ignored.
sync-customer.ts
// Your backend, whenever the facts change: sign-in, plan change, seat added.
const url =
  "https://grahak.dev/api/host/end-users/" +
  encodeURIComponent(user.id) +   // the same id you put in the token's sub
  "/metadata";

await fetch(url, {
  method: "PUT",
  headers: {
    Authorization: "Bearer " + hostApiJwt, // a token with purpose: "host-api"
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    metadata: {
      plan: "Growth",
      seats: 12,
      mrr: 480,
      past_due: false,
      // Flatten lists yourself: a nested value can only be shown as raw JSON.
      projects: "Project 1, Project 2",
    },
  }),
});

The push replaces the whole blob, so send the complete set each time. Grahak never calls you back for it, so what is stored is exactly as fresh as your last push, and the panel says so with an Updated row above the values.

Keep the values flat

Strings, numbers, booleans and null all render as themselves. A nested object or array is stored too, but all the console can do is print its JSON, so ["Project 1", "Project 2"] reaches your team as punctuation rather than a sentence. Join it yourself and send "Project 1, Project 2" instead.

Full details, including the host-API token and examples in Go and Python, are in the host integration guide.

Controlling the widget

Drive it from your own UI: your own button, an unread badge, a theme that follows your app.

  • Open and close. open(), close(), toggle() — or the same from useGrahak() in React.
  • Unread count. Subscribe to the unread event (or useGrahakUnread()) to badge your own button.
  • Theme. setTheme("dark" | "light"), or the theme prop, to match your app.
  • Logout. Call destroy() when your user signs out. In React it happens on unmount.
widget.ts
const widget = init({ projectId, userJwt })
  .mountOverlay({ launcher: false }); // I'll use my own button

myButton.addEventListener("click", () => widget.toggle());

// Badge my own button with the unread count.
widget.on("unread", ({ count }) => renderBadge(count));

// Follow my app's theme, and tear down on logout.
widget.setTheme("dark");
onLogout(() => widget.destroy());

Bring your own button

Prefer not to wire the events yourself? Pass your own element to launcher. Grahak binds its click to toggle() and reflects state onto it, so plain CSS drives the look: it sets data-grahak-open (and aria-expanded) and data-grahak-unread. Your element stays where it is in your DOM — you own its layout.

widget.ts
const myButton = document.querySelector("#support");

init({ projectId, userJwt }).mountOverlay({ launcher: myButton });

// Grahak keeps these in sync on your element — no event wiring needed:
//   data-grahak-open="true | false"    (also sets aria-expanded)
//   data-grahak-unread="true | false"  (true only while closed with unread)
styles.css
#support[data-grahak-open="true"] .chat   { display: none }
#support[data-grahak-open="true"] .close  { display: block }
#support[data-grahak-unread="true"] .dot  { display: block }

API reference

Options — init(options) and <GrahakProvider>

projectId
string · required
Your project's ID from Console.
userJwt
string | () => Promise<string>
A token, or a getter that returns one. Prefer a getter.
anonymous
boolean · false
Allow visitors with no token.
theme
"dark" | "light" · "light"
Widget color scheme. Changing it re-themes live.
mode
"overlay" | "attach" · "overlay"
React only. Overlay adds a launcher; attach renders inline with <GrahakWidget />.
launcher
({ isOpen, unreadCount, toggle, ... }) => ReactNode
React only. Render your own launcher; Grahak suppresses its FAB and docks yours in the same corner, re-rendered with live state.

Overlay — mountOverlay(options) and the overlay prop

position
"bottom-right" | "bottom-left" · "bottom-right"
Corner the launcher and panel sit in.
launcher
boolean | HTMLElement · true
Grahak's launcher button. false to drive it yourself, or pass your own element to adopt it (wired to toggle(), state mirrored via data-grahak-open / data-grahak-unread).
offset
number · 20
Pixels from the viewport edges.
width, height
number · 400, 600
Panel size in pixels (clamped to the viewport).
accentColor
string · "#111827"
Launcher background color.
zIndex
number · 2147483000
Stacking order, in case something sits on top.

Events — widget.on(event, handler)

ready
Handshake complete; the widget is live.
open, close
The panel opened or closed.
unread
{ count }
The unread message count changed.
resize
{ height }
The panel's content height changed.
error
{ message }
Something went wrong (bad token, network).

Ready to wire it up? Grab your project ID and add an identity key in Console.