Guides
Getting Started with Rough in SvelteKit
Sign Rough tokens in a SvelteKit endpoint and show Features on a page.
This page covers the parts of a @roughapp/feature integration that are
specific to SvelteKit and Svelte 5. 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
.envfile. - Choose who shares Features.
Do not give the variables a PUBLIC_ prefix. SvelteKit sends PUBLIC_
variables to the browser, and the private key must stay on the server.
This page assumes your authentication hook sets locals.user for signed-in
users, with its type declared in src/app.d.ts.
Add the token endpoint
This endpoint signs a token for the signed-in user. Create
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' } })
}
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
src/lib/rough/. None of them need changes for SvelteKit.
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.
Load the user for the page
The page needs the Project ID and the IDs that go in the token. Load them on
the server. In src/routes/dashboard/+page.server.ts:
import { env } from '$env/dynamic/private'
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = ({ locals }) => {
if (!locals.user) redirect(303, '/login')
return {
roughProjectId: env.ROUGH_PROJECT_ID ?? '',
userId: locals.user.id,
accountId: locals.user.organisationId,
}
}
The SDK cannot be imported on the server, so turn off server-side rendering
for this page. The server load above still runs. In
src/routes/dashboard/+page.ts:
export const ssr = false
If the page must render on the server, import the SDK and the files in
src/lib/rough/ with await import() inside the effect below instead.
Show Features on the page
The page starts a client, shows the published Features in a div, and adds a
button to create one. In src/routes/dashboard/+page.svelte:
<script lang="ts">
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'
import type { PageProps } from './$types'
let { data }: PageProps = $props()
let element: HTMLDivElement
let createFeature = $state<() => Promise<unknown>>()
$effect(() => {
const client = startClient({
projectId: data.roughProjectId,
userId: data.userId,
accountId: data.accountId,
})
const stopFeatures = showRoughFeatures({ client, surface: dashboardSurface, element })
createFeature = featureCreator(client, dashboardSurface)
return () => {
createFeature = undefined
void stopFeatures()
.then(() => client.destroy())
.catch((error) => console.error('Could not stop Rough', error))
}
})
</script>
<div bind:this={element}></div>
<button
type="button"
disabled={!createFeature}
onclick={() => createFeature?.().catch((error) => console.error(error))}
>
Create a Feature
</button>
The effect reads data, so it runs again if the user or account changes. Each
time, it stops the old client and starts a new one.
If several pages show Features, start the client once in a layout and pass it down, rather than starting one on every page. Check that it works the same way as in the Getting Started guide.