Guides
Getting Started with Rough in Next.js
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:
- Install the SDK.
- Create a project and a signing key,
and put the three variables in your
.env.localfile. - 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.
client.tsstarts the client.surface.tsdefines the Surface and its capabilities.show-features.tskeeps an element in sync with the published Features.builder.tscreates a Feature and opens the Builder.
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.