Getting Started

Connect your app

AI generated The content on this page was written using large language models.

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.

This example assumes your authentication hook puts the signed-in user on locals.user, and that you’ve declared its type in src/app.d.ts. Use your own session lookup if that isn’t how your app works. The fields id, name, email, and organisationId below come from your app, not Rough.

src/routes/api/rough/user-token/+server.ts
import { env } from '$env/dynamic/private'
import { error, json } from '@sveltejs/kit'
import { importPKCS8, SignJWT } from 'jose'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ locals }) => {
  const user = locals.user
  if (!user) error(401, 'Not signed in')

  const {
    ROUGH_PROJECT_ID,
    ROUGH_SIGNING_KEY_ID,
    ROUGH_SIGNING_PRIVATE_KEY_PEM,
  } = env
  if (
    !ROUGH_PROJECT_ID ||
    !ROUGH_SIGNING_KEY_ID ||
    !ROUGH_SIGNING_PRIVATE_KEY_PEM
  ) {
    error(500, 'Rough environment variables are missing')
  }

  // Turn the "\n" sequences in the variable back into line breaks.
  const privateKey = await importPKCS8(
    ROUGH_SIGNING_PRIVATE_KEY_PEM.replaceAll('\\n', '\n'),
    'RS256',
  )

  const token = await new SignJWT({
    rough: {
      user: { name: user.name, email: user.email },
      account: { id: user.organisationId },
    },
  })
    .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)

  return json({ token }, { headers: { 'Cache-Control': 'no-store' } })
}

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.

src/lib/rough/client.ts
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.