Getting Started
Connect your app
Vanilla
Your users are already signed in to your app. We’ll use that session to sign a short-lived token telling Rough who they are and which account they belong to. The browser SDK asks your server for a new token whenever it needs one.
Decide who shares Features
An account is the group of people who should share Features. It uses an ID from your app; you don’t need to create another account in Rough.
| Who should share Features? | Use this account ID |
|---|---|
| Members of the same organisation | Your organisation’s stable ID |
| Each user should have their own Features | The user’s stable ID |
| Everyone in your product | A constant, such as default |
Only use a constant if you really want everyone to share Features. Sharing a Feature doesn’t replace your app’s data permissions: each person’s data access must still be checked by your app.
We’ll use user.organisationId in the examples. Replace it with your chosen account ID everywhere, including the browser client. If users can switch organisations, check their membership before choosing the active account.
Add a token endpoint
The endpoint must read the user and account from your authenticated session, not from a request body or an unverified browser value. Otherwise someone could request a token for another user or account.
The following route belongs in your existing Express app, after its authentication middleware. app is your Express instance, and request.user is the user your middleware authenticated. These aren’t Rough APIs. Adapt the route and session lookup if your backend uses something else; the JWT-signing code stays the same.
import { importPKCS8, SignJWT } from 'jose'
const {
ROUGH_PROJECT_ID,
ROUGH_SIGNING_KEY_ID,
ROUGH_SIGNING_PRIVATE_KEY_PEM,
} = process.env
if (
!ROUGH_PROJECT_ID ||
!ROUGH_SIGNING_KEY_ID ||
!ROUGH_SIGNING_PRIVATE_KEY_PEM
) {
throw new Error('Rough environment variables are missing')
}
// The environment variable stores the key on one line, with "\n" where the line
// breaks were. Put the real line breaks back before importing it.
const privateKey = await importPKCS8(
ROUGH_SIGNING_PRIVATE_KEY_PEM.replaceAll('\\n', '\n'),
'RS256',
)
// Register this after your authentication middleware.
app.post('/api/rough/user-token', async (request, response) => {
const user = request.user // Replace with however your app reads the signed-in user.
if (!user) {
response.sendStatus(401)
return
}
const token = await new SignJWT({
rough: {
user: { name: user.name, email: user.email },
account: { id: user.organisationId, name: user.organisationName },
},
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: ROUGH_SIGNING_KEY_ID })
.setIssuer(ROUGH_PROJECT_ID)
.setSubject(user.id)
.setAudience('rough:session')
.setIssuedAt()
.setExpirationTime('5m')
.sign(privateKey)
response.set('Cache-Control', 'no-store').json({ token })
})
Apply your app’s usual CSRF protection to this cookie-authenticated POST endpoint. Depending on your setup, that may include checking the request’s origin or requiring a CSRF token. If you use a CSRF header, add it to the browser request below too.
What goes in the token
iss: your Rough Project ID.sub: your stable user ID.rough.user.name: the user’s name. This is required; email is optional and must be valid when included.rough.account.id: the stable account ID you chose above. An account name is optional.kid: your signing key ID, in the protected header.aud:rough:session.
Use RS256 and an expiry no more than five minutes after issue, as shown in the example. Don’t cache token responses or log tokens.
Prepare the browser client
This helper starts a client for one project, user, and account. The three IDs must match the token’s iss, sub, and rough.account.id respectively.
import { startRoughClient } from '@roughapp/feature'
export function startClient(options: {
projectId: string
userId: string
accountId: string
}) {
return startRoughClient({
projectId: options.projectId, // The token's iss
externalUserId: options.userId, // The token's sub
externalAccountId: options.accountId, // The token's rough.account.id
fetchUserToken: async () => {
const response = await fetch('/api/rough/user-token', {
method: 'POST',
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Rough token request failed with ${response.status}`)
}
const { token } = (await response.json()) as { token: string }
return token
},
onError: (error) => console.error('Rough authentication failed', error),
})
}
The client starts synchronously and connects in the background. Authentication errors go to onError; use your app’s error reporting there if you have it. For a dark app, pass theme: 'dark' to startRoughClient; you can update it later with client.setTheme().
Keep this module browser-only. The SDK can’t be imported during server rendering. We’ll start it from the page in step 4, after defining the data it can access, and wire up cleanup at the same time. You don’t need to start a temporary client now.
Check authentication
While signed in, make a POST request to /api/rough/user-token from your app using its usual CSRF protections. It should return an object with a token string. Signed out, it should return 401, not a token or your login page’s HTML.
If the request fails, check your session lookup and server environment before moving on. Keep the returned token out of screenshots, logs, and support messages.