Rough

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:

  1. install @roughapp/feature;
  2. create a Rough project and signing key;
  3. add a server endpoint that returns a short-lived user token;
  4. initialize Rough in your browser app;
  5. render your first <rough-surface>;
  6. 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:

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:

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:

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.