Guides
Getting Started with Rough
Add Rough to your product: install the SDK, authenticate your users, and render your first surface.
This guide walks you through adding Rough to your product for the first time. The examples use plain JavaScript and Web Components, so they work in any framework that uses a modern bundler.
In this guide, you will:
- install
@roughapp/feature; - create a Rough project and signing key;
- add a server endpoint that returns a short-lived user token;
- initialize Rough in your browser app;
- render your first
<rough-surface>; - add tools so features can read or write approved data.
Before you start
@roughapp/feature is ESM-only and is designed for apps built
with npm and a modern bundler such as Vite, Webpack, or Rspack. Direct <script> tags and CDN usage are not supported yet.
Install the SDK
Add Rough and zod to your app.
You will use zod later to describe the inputs and outputs for
your tools.
npm install @roughapp/feature zod Create a Rough project and signing key
Create a Rough account at in.rough.app/signup.
Click New Project in the left-hand sidebar, enter a project name, and click Create Project. A project represents one product or app. Copy the Project ID at the top of the page; you will use it when you initialize the SDK.
Next, create a Signing Key. Click your workspace name in the top-left corner and select Workspace Settings, then Signing Keys, then Create Signing Key. Enter a name, click Create, and save the private key somewhere safe. It is only shown once.
Server: return a Rough user token
Your signing key must stay on your server. To authenticate a user, add an endpoint in your app that signs a short-lived identity token. The Rough SDK will call this endpoint from the browser whenever it needs to prove who the current user is.
The endpoint does three things:
- checks that the request belongs to a logged-in user;
- signs a JSON Web Token (JWT) with your private signing key;
- describes the current user and account to Rough.
This example uses jose with Express, but the same pattern works in any server framework.
import { SignJWT, importPKCS8 } from 'jose'
// Keep these on the server, outside your frontend bundle and source code.
const ROUGH_PROJECT_ID = process.env.ROUGH_PROJECT_ID
const ROUGH_SIGNING_KEY_ID = process.env.ROUGH_SIGNING_KEY_ID
const ROUGH_SIGNING_PRIVATE_KEY = process.env.ROUGH_SIGNING_PRIVATE_KEY
app.post('/api/rough-user-token', async (request, response) => {
// Replace this with however your app reads the logged-in user.
const user = request.user
if (!user) {
response.status(401).end()
return
}
const privateKey = await importPKCS8(ROUGH_SIGNING_PRIVATE_KEY, 'RS256')
const token = await new SignJWT({
rough: {
user: {
name: user.fullName,
email: user.email, // optional
},
account: {
// Use a stable ID for the customer's account/team/workspace.
// If your product is not multi-tenant, use a constant like 'default'.
id: user.organisationId,
name: user.organisationName, // optional
},
},
})
.setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: ROUGH_SIGNING_KEY_ID })
.setIssuer(ROUGH_PROJECT_ID)
.setSubject(user.id) // stable user ID from your system
.setAudience('rough:session')
.setIssuedAt()
.setExpirationTime('5m')
.sign(privateKey)
response
.set('Cache-Control', 'no-store')
.type('text/plain')
.send(token)
}) Keep these details stable:
setSubject(user.id)should use a stable user ID from your product.rough.account.idshould use a stable account, team, or workspace ID. Rough uses it to keep each account's features separate.- Tokens must include
iatandexp, andexpmust be no more than 5 minutes afteriat.
Browser: initialize Rough
Import the Rough stylesheet once near your app's browser entry point. This adds Rough theme variables; component styles are loaded by the custom elements.
import '@roughapp/feature/style.css' Then call initRough. The projectId is the Project
ID you copied earlier. fetchUserToken should call the server
endpoint you just created.
import { initRough } from '@roughapp/feature'
initRough({
projectId: 'your-project-id',
fetchUserToken: async () => {
const response = await fetch('/api/rough-user-token', { method: 'POST' })
if (!response.ok) {
throw new Error('Could not fetch Rough user token')
}
return response.text()
},
}) Browser: define and render a surface
A surface is a place in your product where Rough features
can appear. You choose the surface key, name, description, and tool list.
Keep the key stable; changing it registers a different surface.
import '@roughapp/feature'
import { defineSurface } from '@roughapp/feature'
const dashboardSurface = defineSurface({
key: 'dashboard',
name: 'Dashboard',
description: 'The main dashboard view',
toolList: [], // start empty; add tools later
})
const slot = document.querySelector('#rough-surface-slot')
const el = document.createElement('rough-surface')
el.surface = dashboardSurface
slot?.append(el) Add an element such as <div id="rough-surface-slot"></div> wherever you want the surface to render.
Let users create features
Rendering <rough-surface> shows features that have already
been published to that surface. To let users build a new feature, call openRoughCreate from a button or menu in your UI.
import { openRoughCreate } from '@roughapp/feature'
document.querySelector('#create-feature')?.addEventListener('click', () => {
openRoughCreate(dashboardSurface)
}) Call openRoughCreate after the surface has rendered. Rendering
the surface registers it with Rough, and registration happens asynchronously.
If you need the published feature list directly, subscribe with getRoughFeatures. It returns a function you can call when you no
longer need updates.
import { getRoughFeatures } from '@roughapp/feature'
const unsubscribe = getRoughFeatures(dashboardSurface, (features) => {
console.log(features.length + ' features on this surface')
})
// Later, when this part of your UI unmounts:
unsubscribe() Add your first tool
A surface with no tools can still show features, but those features cannot read from or write to your app. Tools give a feature safe, explicit access to the data and actions you choose.
There are three kinds of tool:
- Query — read something once, such as the current user.
- Subscription — read something and keep receiving updates.
- Mutation — change something, such as renaming a project.
Start with a Query. Each tool has an id, a
human-readable name and description, input and
output schemas, an outputSample, and an implementation that runs in your app.
import { Query } from '@roughapp/feature'
import { z } from 'zod'
const getCurrentUser = new Query({
id: 'getCurrentUser',
name: 'Get current user',
description: 'Returns the user currently signed in.',
inputSchema: z.object({}),
outputSchema: z.object({
id: z.string(),
name: z.string(),
}),
outputSample: { id: 'user_1', name: 'Ada Lovelace' },
implementation: async () => {
const user = await myApp.getCurrentUser()
return { id: user.id, name: user.name }
},
}) Then add the tool to your surface:
const dashboardSurface = defineSurface({
key: 'dashboard',
name: 'Dashboard',
description: 'The main dashboard view',
toolList: [getCurrentUser],
}) Features on the dashboard can now read the current user, and nothing else.
Add a Query, Subscription, or Mutation for each piece of data or action you want features to use.