Skip to content

Identify users

Use Jotly('identify', …) to tell Jotly who the signed-in user is. Once identified, their conversations, votes and ideas are tied to them, your team sees their context, and they can enter your public portal without signing in again.

There are four mechanisms, and they all use your workspace's identity secret:

  • JWT token (recommended): identify({ token }), signed on your server. Result: signed.
  • HMAC signature of the id: identify({ id, signature }). Result: signed.
  • Unsigned: identify({ id }). Result: unsigned; fine to get started, not for sensitive data.
  • Portal SSO (Pro plan): the portal's Sign in button sends users to your app and they come back with a JWT.

Basic identification

Call identify after init, once your product knows who's signed in:

Jotly('identify', {  id: 'user_123',  email: 'ana@company.com',  name: 'Ana López',});

Only id is required; everything else is optional, though sending email is almost always worth it. Use the user id from your own app: the stable identifier you assign in your system, not a session id or a value that changes between sign-ins.

Besides the id, you can send their avatar, company, monthly spend and custom attributes your team will see next to every conversation:

Jotly('identify', {  id: 'user_123',                 // required: your user id  email: 'ana@company.com',  name: 'Ana López',  avatar: 'https://…/ana.png',  company: 'Acme Inc',  monthlySpend: 49,  attributes: { plan: 'pro', country: 'US' },  signature: 'HMAC_OF_THE_ID',    // see "Sign the id with HMAC-SHA256"});

In most apps, identify the user when:

  • they sign in
  • your app restores an existing session
  • they update their profile and you want Jotly to reflect the new data

Signed identity with a JWT

Without a signature, anyone could type a different id into the browser console. With a token, Jotly verifies that the identity comes from your server. Generate an HS256 JWT with the secret from Settings → Sign-in and pass it as token. identify returns a promise with the result:

// Your server generates the token and hands it to the page.const { viewer, identityTrust, warnings } = await Jotly('identify', {  token: tokenFromYourServer,}); console.log(identityTrust); // 'signed'

Token claims

ClaimRequiredDescription
subYesYour user id (string).
issYesYour Jotly workspace slug or id.
audYes"jotly"
iatYesUnix seconds when you signed it.
expYesExpiration; at most 1 hour after iat. We allow 60 s of clock skew.
emailNoThe user's email.
firstName, lastNameNoFirst and last name.
avatarNoHTTPS URL of their photo.
traitsNoAttributes with string, number, boolean or null values.
companiesNoList of { id, name?, domain?, traits? }.

Data inside the token is verified. If you also send loose fields (email, name…), they're unverified extra info, and the token wins when they don't match.

import crypto from "node:crypto"; // On YOUR server, never in the browser.const SECRET = process.env.JOTLY_SECRET;const WORKSPACE = "your-workspace"; // your Jotly workspace slugconst b64 = (v) => Buffer.from(JSON.stringify(v)).toString("base64url"); export function jotlyToken(user) {  const now = Math.floor(Date.now() / 1000);  const payload =    b64({ alg: "HS256", typ: "JWT" }) +    "." +    b64({      sub: String(user.id),   // your user id      iss: WORKSPACE,         // workspace slug or id      aud: "jotly",      iat: now,      exp: now + 10 * 60,     // at most 1 hour after iat      email: user.email,      firstName: user.firstName,      lastName: user.lastName,      avatar: user.avatarUrl,      traits: { plan: user.plan },      companies: [{ id: String(user.companyId), name: user.companyName }],    });  const signature = crypto.createHmac("sha256", SECRET).update(payload).digest("base64url");  return payload + "." + signature;}
An invalid or expired token, or one with the wrong aud or iss, is never accepted as an unsigned identity: identify rejects the promise with IDENTIFY_FAILED and the API responds 401 with a code like TOKEN_EXPIRED, TOKEN_SIGNATURE, TOKEN_AUDIENCE or TOKEN_ISSUER. Generate a fresh token on every page load.

Sign the id with HMAC-SHA256

The alternative to a token is to sign just the id: HMAC-SHA256(id, secret) in hex, in the same identify call. The result is also signed, but only the id is verified, not the other fields.

Jotly('identify', {  id: 'user_123',  email: 'ana@company.com',  signature: 'HMAC_OF_THE_ID', // computed on your server});
import crypto from "node:crypto"; // On YOUR server, never in the browser.const SECRET = process.env.JOTLY_SECRET; export function signJotly(userId) {  return crypto.createHmac("sha256", SECRET).update(String(userId)).digest("hex");}
If you send a signature that doesn't match, Jotly rejects the identification. Make sure you sign exactly the same id, as a string, that you pass to identify.

Require signed identity

In Settings → Sign-in you can turn on Require signed identity. With it on, any identify call without a valid token or signature is rejected with SIGNED_IDENTITY_REQUIRED, and automatic sign-in to the portal only accepts JWTs. A user who was verified once can't get back in without a signature either.

Sign in to the public portal

Your feedback portal offers three ways to sign in, under Settings → Sign-in → Portal sign-in:

  • Email link (default): the user enters their email and gets a magic link and a 6-digit code. Both work once and expire in 15 minutes. Entering someone else's email grants access to nothing.
  • Your app's SSO (Pro plan): the Sign in button sends users to your app, which signs a JWT.
  • Disabled (Pro plan): there's no Sign in button. Guests can take part if you allow them, along with anyone who arrives through automatic sign-in.

With Post as guest on, visitors can also post, vote and comment without an account. Each guest is a new identity that can only see and edit their own posts while their session lasts in that browser.

Portal SSO

The flow works like any redirect-based SSO:

  • The user clicks Sign in and Jotly sends them to your sign-in URL with ?state=…. The state is single-use and expires in 15 minutes.
  • Your app authenticates them as usual (or recognizes them if they already have a session).
  • Your server signs a JWT with the same claims as above (here iat is optional; we suggest a lifetime of 5 to 15 minutes) and redirects to /api/portal/your-workspace/sso?jwt=…&state=… with the same state. The exact URL is in Settings.
import express from "express";import { jotlyToken } from "./jotly.js"; // the function above const app = express(); // Sign-in URL you entered in Jotly: https://yourapp.com/jotly/ssoapp.get("/jotly/sso", requireLogin, (req, res) => {  const state = String(req.query.state ?? "");  const jwt = jotlyToken(req.user);  const back = new URL("https://jotly.si/api/portal/your-workspace/sso");  back.searchParams.set("jwt", jwt);  back.searchParams.set("state", state); // the same state, unchanged  res.redirect(back.toString());});

If something fails, the user returns to the portal with ?sso_error= and one of these values: invalid_state (the state doesn't exist, was already used or expired), invalid_token (the JWT's signature, aud, iss or expiration) or auth_failed (SSO is off or the account can't take part).

Automatic sign-in from your app

If your app runs the widget with an identified user, any link in your app that points to your portal gets ?clientToken= appended and the user arrives already signed in. There's nothing else to do: the widget rewrites the link right when the user opens it.

// clientToken = base64 of this JSON (not a secret: trust comes from the token inside){  "version": 2,  "id": "your-workspace",  "token": "eyJhbGciOiJIUzI1NiJ9…"   // or "user": { "id", "email", "name", "signature" }}

With Require signed identity on, only clientToken values that carry a JWT are accepted. Automatic sign-in doesn't apply to your team's accounts.

Log out

When the user signs out of your app, forget them so the next person using the browser doesn't see their history:

// When the user signs out of your appJotly('logout');

If you haven't installed the widget yet, start with Installation.

See also Feedback widget in your app