Guides

Getting Started with Rough in SvelteKit

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

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:

  1. Install the SDK.
  2. Create a project and a signing key, and put the three variables in your .env file.
  3. 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.

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.