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;
- create a Rough client in your browser app;
- subscribe to published features and render their frames;
- add capabilities 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.
The package's root module is browser-only because it registers custom elements when evaluated. If your framework uses server-side rendering, import Rough and create the client from a client-only boundary.
Install the SDK
Add Rough and zod to your app.
You will use zod later to describe the inputs and outputs for
your capabilities.
npm install @roughapp/[email protected] zod The SDK is pre-1.0, so this guide pins the current version. Review the Feature SDK changelog before upgrading.
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 and click Create. Copy the Key ID (kid) and private key somewhere safe. Rough only shows the private key once. Select Environment variables to copy the variable names used below.
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. Install jose in your server app before using it.
npm install jose 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_PEM = process.env.ROUGH_SIGNING_PRIVATE_KEY_PEM
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
}
// Environment-variable UIs commonly preserve escaped newlines literally.
const privateKeyPem = ROUGH_SIGNING_PRIVATE_KEY_PEM.replaceAll('\\n', '\n')
const privateKey = await importPKCS8(privateKeyPem, '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: create a client
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 create a client. The projectId is the Project ID you
copied earlier. fetchUserToken should call the server endpoint
you just created.
import { createRoughClient } from '@roughapp/feature'
const client = createRoughClient({
projectId: 'your-project-id',
fetchUserToken: async () => {
const response = await fetch('/api/rough-user-token', {
method: 'POST',
credentials: 'include',
})
if (!response.ok) {
throw new Error('Could not fetch Rough user token')
}
return response.text()
},
}) The client starts connecting straight away. Create it wherever your app owns this project, share that one client with everything below it, and destroy it when the project goes away.
await client.destroy() Browser: define a surface and render its features
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 { defineRoughSurface, getRoughFeatures } from '@roughapp/feature'
const dashboardSurface = defineRoughSurface({
key: 'dashboard',
name: 'Dashboard',
description: 'The main dashboard view',
toolList: [], // start empty; add capabilities later
})
const slot = document.querySelector('#rough-feature-slot')
const subscription = getRoughFeatures({
client,
surface: dashboardSurface,
onFeatures: (features) => {
slot?.replaceChildren(
...features.flatMap((feature) => {
if (!feature.publishedBuildId) return []
const frame = document.createElement('rough-feature')
frame.client = client
frame.surface = dashboardSurface
frame.featureId = feature.id
frame.buildId = feature.publishedBuildId
return [frame]
}),
)
},
onError: (error) => console.error('Unable to load Rough Features', error),
})
// Later, when this part of your UI unmounts:
await subscription.unsubscribe() Add an element such as <div id="rough-feature-slot"></div> wherever you want feature frames to render. Your app owns their layout and
any surrounding UI.
Let users create features
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', async () => {
const modal = await openRoughCreate({ client, surface: dashboardSurface })
// The modal closes itself; call this if you need to close it from your code.
// await modal.close()
}) You can call openRoughCreate at any time. It does not require
a feature frame to have rendered first.
The subscription's ready promise resolves after the first
feature list arrives. Unsubscribe it when the part of your UI that renders
the frames unmounts.
Add your first capability
A surface with no capabilities can still show features, but those features cannot read from or write to your app. Capabilities are represented in code by tools, which give a feature access to the data and actions you choose, and to nothing else.
There are three kinds of capability:
- Query reads something once, such as the current user.
- Subscription reads something and keeps receiving updates.
- Mutation changes 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's toolList:
const dashboardSurface = defineRoughSurface({
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.