With single sign-on (SSO), people signed in to your app are signed in on your portal and in the widget too, without a separate account. Your server signs a short-lived token for the user and sends them to your portal. SSO works on every plan. Owners and admins set it up in Settings → Single sign-on.
1. Enable SSO
Open Settings → Single sign-on and choose Enable SSO.
Copy the secret. It starts with
tsso_and is shown only once.Keep it on your server only, for example as
TUTI_SSO_SECRET. Never send it to the browser.

Rotate secret makes a new secret. Tokens signed with the old one stop working at once, so update your server straight away.
Disable turns SSO off. Your app's sign-in links stop working at once. People can still sign in on the portal with email.
2. Sign a token on your server
The token is a JWT signed with HS256 using the secret.
Claim | Required | What it's for |
|---|---|---|
| Yes | Your user's ID. Tuti knows the user by it, so keep it stable. |
| Yes | A unique ID per token, such as a UUID. Each token works once. |
| Yes | When it was made. |
| Yes | When it expires: at most 10 minutes after |
| No | The user's email address. See "The email claim" below. |
| No | Shown on their posts and comments. Without it, the part of the email before the |
| No | An |
| No | Their groups in your app, like |
| No | What you know about the user, like |
In Node, with the jose library:
import { SignJWT } from "jose";
const token = await new SignJWT({
email: user.email,
name: user.name,
groups: user.isBetaTester ? ["beta"] : [],
properties: { plan: user.plan, role: user.role },
})
.setProtectedHeader({ alg: "HS256" })
.setSubject(String(user.id))
.setJti(crypto.randomUUID())
.setIssuedAt()
.setExpirationTime("5m")
.sign(new TextEncoder().encode(process.env.TUTI_SSO_SECRET));Any JWT library works the same way.
Properties
properties is an object of what your app knows about the user, by name. Reader groups match their conditions against it, and Users shows it. See See your users and the companies they're at.
Names are up to 64 characters. Values are text (up to 200 characters), numbers, true or false, or lists of up to 50 pieces of text.
A user has up to 50 properties. New ones past that are left out.
Each sign-in sets the properties it names and keeps the rest. Send a property as
nullto remove it. A token withoutpropertieschanges none.A property Tuti can't read, like an object or a list of numbers, is left out on its own. It never stops the user signing in.
attributes, the old name forproperties, still works.
groups is stored as the groups property, up to 20 per user. Each is up to 50 letters, digits and -, _, . or :, starting with a letter or digit, and Tuti keeps it in lowercase. Unlike properties, a group Tuti can't read stops the sign-in (invalid_claims). A token with groups replaces the ones Tuti has, and [] removes them.
You can also send properties without a sign-in, through the API's users.sync. See See your users and the companies they're at.
The email claim
Tuti never uses email to sign anyone in: users are matched by sub only. But the address counts in two places:
Someone who signs in to your portal another way, with that address verified, gets this user's groups.
In the widget's Messages, the user sees the conversations emailed to your support address from that address.
So send only an address your app has verified. See Let people message your team from the widget.
3. Send the user to your portal
Send them to your portal's /sso with the token:
https://your-project.tuti.so/sso?token=<token>&redirect=<path>The exact address is in Settings → Single sign-on. It works on your custom domain or sub-folder too.
redirectis optional: a path on your portal, such as/feedback/feature-requests. Without it, or if it isn't a path on your portal, they land on the portal's home.Make the token right before sending them. Tokens expire quickly and only work once, so don't put them in emails or cached pages.
A common pattern is a "Feedback" link in your app to a route that signs a token for the current user and redirects.
Add "Continue with" to the portal's sign-in page
Once SSO is on, Your app's sign-in page adds a Continue with button, with your project's name, to the portal's sign-in page.
Enter an
https://address in your app, likehttps://app.yourcompany.com/tuti, and choose Save.The button sends people there with
redirect, the portal page to come back to.Sign them in to your app if they aren't yet, then send them to
/ssoas in step 3, with thatredirect.
Leave the address empty to take the button away. See how it looks in Make your portal private.
Sign people in to the widget
The widget runs on your site, where your portal's sign-in can't reach it. Give it a way to get a fresh token from your server:
<script>
window.TutiSettings = {
identify: () => fetch("/tuti-token").then((response) => response.text()),
};
</script>
<script src="https://tuti.so/widget.js" data-project="your-project" async></script>/tuti-token is a route in your app that signs a token as in step 2. People your app signs in can then post, vote and message your team from the widget as themselves, and read a private portal there. The widget keeps their sign-in for up to 12 hours. See Open the widget from your own code.
If sign-in fails
The user sees "Couldn't sign you in" on your portal, with a reason and a code:
Code | What's wrong |
|---|---|
| SSO is off for this project. |
| The token isn't signed with the project's secret using HS256, it's malformed, or it lacks |
|
|
|
|
| A token with this |
| A claim has the wrong type or form, like an |
Keep it safe
Each
subis its own user on your portal. Your tokens can't sign anyone in as another project's user or an existing Tuti account, even with the same email.Signing a token is the same as signing in as that user on your portal, so protect the secret like a password.

