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
| Claim | Required | Description |
|---|---|---|
sub | Yes | Your user id (string). |
iss | Yes | Your Jotly workspace slug or id. |
aud | Yes | "jotly" |
iat | Yes | Unix seconds when you signed it. |
exp | Yes | Expiration; at most 1 hour after iat. We allow 60 s of clock skew. |
email | No | The user's email. |
firstName, lastName | No | First and last name. |
avatar | No | HTTPS URL of their photo. |
traits | No | Attributes with string, number, boolean or null values. |
companies | No | List 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;}import { SignJWT } from "jose"; const secret = new TextEncoder().encode(process.env.JOTLY_SECRET); export async function jotlyToken(user) { return new SignJWT({ email: user.email, firstName: user.firstName, lastName: user.lastName }) .setProtectedHeader({ alg: "HS256" }) .setSubject(String(user.id)) .setIssuer("your-workspace") .setAudience("jotly") .setIssuedAt() .setExpirationTime("10m") .sign(secret);}An invalid or expired token, or one with the wrongaudoriss, is never accepted as an unsigned identity:identifyrejects the promise withIDENTIFY_FAILEDand the API responds401with a code likeTOKEN_EXPIRED,TOKEN_SIGNATURE,TOKEN_AUDIENCEorTOKEN_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");}import hmac, hashlib, os SECRET = os.environ["JOTLY_SECRET"].encode() def sign_jotly(user_id: str) -> str: return hmac.new(SECRET, str(user_id).encode(), hashlib.sha256).hexdigest()<?phpfunction sign_jotly(string $userId): string { return hash_hmac('sha256', $userId, getenv('JOTLY_SECRET'));}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=…. Thestateis 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
iatis optional; we suggest a lifetime of 5 to 15 minutes) and redirects to/api/portal/your-workspace/sso?jwt=…&state=…with the samestate. 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