Guides

Getting Started with Rough in Next.js

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

Sign Rough tokens in a Route Handler and show Features in a Client Component.

This page covers the parts of a @roughapp/feature integration that are specific to the Next.js App Router. It builds on the Getting Started guide, which explains each step in more detail.

Set up your project

Follow these steps from the Getting Started guide first:

  1. Install the SDK.
  2. Create a project and a signing key, and put the three variables in your .env.local file.
  3. Choose who shares Features.

Do not give the variables a NEXT_PUBLIC_ prefix. Next.js copies NEXT_PUBLIC_ variables into the browser bundle, and the private key must stay on the server. The page below passes the Project ID to the browser as a prop instead.

The examples call getSession() from @/lib/auth. Replace it with your own authentication library.

Add the token Route Handler

This Route Handler signs a token for the signed-in user. Create app/api/rough/user-token/route.ts:

import { importPKCS8, SignJWT } from 'jose'
import { getSession } from '@/lib/auth'

export async function POST() {
  const session = await getSession()
  if (!session) return new Response('Not signed in', { status: 401 })

  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) {
    return new Response('Rough environment variables are missing', { status: 500 })
  }

  // 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 { user } = session
  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 Response.json({ token }, { headers: { 'Cache-Control': 'no-store' } })
}

Use the same account ID here and in the page below. See what Rough checks in the token.

Add the browser files

Copy the four helper files from the Getting Started guide into lib/rough/. None of them need changes for Next.js.

Show Features in a Client Component

This component starts a client, shows the published Features in a div, and adds a button to create one. Create app/dashboard/rough-dashboard.tsx:

'use client'

import { useEffect, useRef, useState } from 'react'
import { featureCreator } from '@/lib/rough/builder'
import { startClient } from '@/lib/rough/client'
import { showRoughFeatures } from '@/lib/rough/show-features'
import { dashboardSurface } from '@/lib/rough/surface'

type Props = { projectId: string; userId: string; accountId: string }

export default function RoughDashboard({ projectId, userId, accountId }: Props) {
  const element = useRef<HTMLDivElement>(null)
  const [createFeature, setCreateFeature] = useState<() => Promise<unknown>>()

  useEffect(() => {
    const client = startClient({ projectId, userId, accountId })
    const stopFeatures = showRoughFeatures({
      client,
      surface: dashboardSurface,
      element: element.current!,
    })
    setCreateFeature(() => featureCreator(client, dashboardSurface))

    return () => {
      setCreateFeature(undefined)
      void stopFeatures()
        .then(() => client.destroy())
        .catch((error) => console.error('Could not stop Rough', error))
    }
  }, [projectId, userId, accountId])

  return (
    <>
      <div ref={element} />
      <button
        type="button"
        disabled={!createFeature}
        onClick={() => createFeature?.().catch((error) => console.error(error))}
      >
        Create a Feature
      </button>
    </>
  )
}

The effect starts a new client whenever the user or account changes, and stops the old one. In development, React Strict Mode runs the effect twice, so you will see one client start, stop, and start again. That is expected.

Add the page

Next.js renders Client Components on the server too, and the SDK cannot be imported there. Load the component with ssr: false so it only runs in the browser. ssr: false only works inside a Client Component, so it needs a small file of its own. Create app/dashboard/rough-dashboard-loader.tsx:

'use client'

import dynamic from 'next/dynamic'

export const RoughDashboardLoader = dynamic(() => import('./rough-dashboard'), {
  ssr: false,
})

Then create the page in app/dashboard/page.tsx. It is a Server Component, so it can read the session and the Project ID:

import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { RoughDashboardLoader } from './rough-dashboard-loader'

export default async function DashboardPage() {
  const session = await getSession()
  if (!session) redirect('/login')

  return (
    <RoughDashboardLoader
      projectId={process.env.ROUGH_PROJECT_ID ?? ''}
      userId={session.user.id}
      accountId={session.user.organisationId}
    />
  )
}

If several pages show Features, start the client once in a shared component and pass it down, rather than starting one on every page. Check that it works the same way as in the Getting Started guide.