Reference

SDK Reference

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

Browser API for @roughapp/feature: clients, Surfaces, capabilities, Features and the Builder.

This page lists every function and class that @roughapp/feature exports, followed by its types. The signatures are simplified to make them easier to read. For a step-by-step integration, start with the Getting Started guide.

The SDK is ESM-only and runs only in the browser. Importing it during server-side rendering fails with HTMLElement is not defined. Its styles are bundled, so there is no stylesheet to import. Read the changelog before upgrading.

startRoughClient(options)

Starts a client and returns it straight away. The client connects and authenticates in the background. Use one client per project, account and signed-in user, and start a new one when any of those changes.

startRoughClient(options: StartRoughClientOptions): RoughClient
// StartRoughClientOptions: projectId, externalUserId, externalAccountId,
// fetchUserToken: () => string | Promise<string>, onError?, baseUrl?, mediaBaseUrl?
Option Meaning
projectId Required Rough Project ID (also the identity token issuer).
externalUserId Required stable user ID; must match the token subject (sub).
externalAccountId Required stable account ID; must match rough.account.id in the token.
fetchUserToken Required. Returns the identity token string from your server's token endpoint. It can be async. Never put the signing key in the browser.
onError Optional. Called with an Error when authentication fails, including when the token's iss, sub or rough.account.id does not match the options above.
baseUrl / mediaBaseUrl Optional gateway and CDN overrides; default to https://gateway.rough.app and https://cdn.rough.app.

A missing option or an invalid URL throws a TypeError straight away. Before sending a token to Rough, the client checks that its iss, sub and rough.account.id match projectId, externalUserId and externalAccountId. See what Rough checks in the token.

The client has one public method, destroy(): Promise<void>. It closes the client's subscriptions and open Builders, and is safe to call more than once. It does not unmount Features you mounted with mountRoughFeature, so unmount those first. The exported RoughClient is a type-only handle, not a constructor. Keep the value returned by startRoughClient() and pass it to SDK operations; internal services such as store, replicache, auth and surfaces are not part of the public type. Do not construct or mock a client object directly.

import { startRoughClient } from '@roughapp/feature'

const client = startRoughClient({
  projectId: 'proj_123',
  externalUserId: signedInUser.id,
  externalAccountId: signedInUser.organisationId,
  fetchUserToken: async () => {
    const response = await fetch('/api/rough/user-token', {
      method: 'POST',
      credentials: 'include',
    })
    if (!response.ok) throw new Error('Could not get a Rough token')
    const { token } = (await response.json()) as { token: string }
    return token
  },
  onError: (error) => console.error(error),
})

// Later, after unmounting your Features:
await client.destroy()

defineRoughSurface(options)

Defines a Surface: a place in your app where Features can appear, and the capabilities they can use there. key, name and description must be non-empty strings. toolList is an array of Query, Mutation and Subscription instances. The result is frozen and does not belong to any client, so you can define it once at module level and share it. Changing the key creates a different Surface, and removing or changing a capability can break Features that use it.

defineRoughSurface<const Key extends string, const ToolList extends readonly (Query | Mutation | Subscription)[]>(
  options: { key: Key; name: string; description: string; toolList: ToolList },
): RoughSurfaceDefinition<Key, ToolList>
import { defineRoughSurface, Query } from '@roughapp/feature'
import { z } from 'zod'

const surface = defineRoughSurface({
  key: 'inbox',
  name: 'Inbox',
  description: 'Messages for the signed-in user',
  toolList: [
    new Query({
      id: 'listMessages',
      name: 'List messages',
      description: 'Read recent messages',
      inputSchema: z.object({ limit: z.number() }),
      outputSchema: z.array(z.string()),
      outputSample: ['Welcome'],
      implementation: async ({ limit }) => fetchMessages(limit),
    }),
  ],
})

Query, Mutation and Subscription

The three kinds of capability. Each takes an id, a name, a description, Zod 4 input and output schemas, an outputSample and an implementation.

The implementation runs in your app, so check permissions there as you would for any other request. Rough only receives the schemas and the sample. The Builder shows the sample while a user builds a Feature, so use made-up values, not customer data. Input and output are checked against the schemas at runtime.

// Shared constructor options (Zod schemas determine input/output types):
type CapabilityOptions<Id extends string, InputSchema extends z.ZodType, OutputSchema extends z.ZodType> = {
  id: Id
  name: string
  description: string
  inputSchema: InputSchema
  outputSchema: OutputSchema
  outputSample: z.infer<OutputSchema>
}

// In the signatures below: Id extends string; InputSchema and OutputSchema extend z.ZodType.
new Query(options: CapabilityOptions<Id, InputSchema, OutputSchema> & {
  implementation: (input: z.output<InputSchema>) => Promise<z.input<OutputSchema>>
}): Query<Id, InputSchema, OutputSchema>
new Mutation(options: CapabilityOptions<Id, InputSchema, OutputSchema> & {
  implementation: (input: z.output<InputSchema>) => Promise<z.input<OutputSchema>>
}): Mutation<Id, InputSchema, OutputSchema>
new Subscription(options: CapabilityOptions<Id, InputSchema, OutputSchema> & {
  implementation: (
    emit: (output: z.input<OutputSchema>) => void,
    input: z.output<InputSchema>,
  ) => Promise<() => void>
}): Subscription<Id, InputSchema, OutputSchema>
Constructor Implementation contract
new Query(options) Read once: async (parsedInput) => output.
new Mutation(options) Write once: async (parsedInput) => output.
new Subscription(options) Ongoing updates: async (emit, parsedInput) => unsubscribeFn. Call emit(output) for each new value; return a function that stops updates.

Query.create(options), Mutation.create(options) and Subscription.create(options) do the same as new. An example of each:

import { Query, Mutation, Subscription } from '@roughapp/feature'
import { z } from 'zod'

const listMessages = new Query({
  id: 'listMessages', name: 'List messages', description: 'Read the inbox',
  inputSchema: z.object({ limit: z.number() }),
  outputSchema: z.array(z.string()), outputSample: ['Welcome'],
  implementation: async ({ limit }) => fetchMessages(limit),
})

const markRead = new Mutation({
  id: 'markRead', name: 'Mark read', description: 'Mark a message as read',
  inputSchema: z.object({ id: z.string() }),
  outputSchema: z.object({ success: z.boolean() }),
  outputSample: { success: true },
  implementation: async ({ id }) => {
    await markMessageRead(id) // Your app's authorized write operation.
    return { success: true }
  },
})

const watchUnreadCount = new Subscription({
  id: 'watchUnreadCount', name: 'Unread count',
  description: 'Watch the unread message count',
  inputSchema: z.object({}), outputSchema: z.number(), outputSample: 3,
  implementation: async (emit, _input) => {
    // Your app's listener calls emit on each change and returns cleanup.
    return watchInbox((count) => emit(count))
  },
})

subscribeToRoughFeatures(options)

subscribeToRoughFeatures(options: {
  client: RoughClient
  surface: RoughSurfaceDefinition
  onChange: (features: RoughFeatureMetadata[]) => void
  onError?: (error: Error) => void
}): RoughFeatureSubscription // { ready: Promise<void>; unsubscribe: () => Promise<void> }

Tells you which Features are published on a Surface. onChange runs once with the current list, which may be empty, and again whenever the list changes. Each item has id, name and publishedBuildId. Drafts and unpublished Features are not included. Unpublishing a Feature in the management console removes it from the subscription's list.

ready resolves after the first list arrives, and rejects if the subscription could not start. onError receives the same error. unsubscribe() is safe to call more than once.

import { subscribeToRoughFeatures } from '@roughapp/feature'

const subscription = subscribeToRoughFeatures({
  client, surface,
  onChange(features) {
    console.log('Published Features:', features)
  },
  onError: (error) => console.error(error),
})
await subscription.ready
// When the owning view is removed:
await subscription.unsubscribe()

mountRoughFeature(container, props)

mountRoughFeature(container: HTMLElement, props: {
  client: RoughClient
  surface: RoughSurfaceDefinition
  featureId: string
  buildId?: string
  datastore?: RoughFrameDatastore
  isEditable?: boolean
  onfeaturepublished?: (detail: FeaturePublishedDetail) => void
}): RoughFeatureMount // { update: (partialProps) => void; unmount: () => Promise<void> }

Renders a Feature inside an element you own. The Feature lives in a shadow root inside that element.

  • Leave out buildId to show the published version. The Feature then updates itself when a new version is published. If the Feature is unpublished, it stops displaying the build and shows an unpublished status message. Pass a buildId to pin one version; an explicitly selected build can still be displayed after the Feature is unpublished.
  • isEditable defaults to true, which shows an edit button when the user hovers over the Feature. Set it to false to hide the button. This is not a permission check.
  • onfeaturepublished is called with { feature, surfaceKey } when the user publishes from that edit button.
  • By default, a Feature saves its own state in the host browser's local storage, keyed only by Feature ID—not by user or account. Users opening the same Feature in the same browser origin share that stored state, even after switching users. This is separate from the user/account-scoped metadata cache. For user/account isolation, shared state across browsers, or your own persistence, pass a datastore with get(), set(state) and addListener(callback). Keep the same object for as long as the Feature is mounted, and use separate state per Feature and, when needed, per user/account.

update(props) changes props without remounting. Pass { buildId: undefined } to go back to the published version. unmount() removes the Feature; await it before you remove the element. It is safe to call more than once.

import { mountRoughFeature } from '@roughapp/feature'

const container = document.querySelector<HTMLElement>('#feature-slot')!
const mount = mountRoughFeature(container, {
  client, surface, featureId: feature.id,
  isEditable: true,
  onfeaturepublished: ({ feature }) => console.log(feature.name),
})
mount.update({ isEditable: false })
// When leaving the view:
await mount.unmount()

createRoughFeature(options)

createRoughFeature(options: {
  client: RoughClient
  surface: RoughSurfaceDefinition
}): Promise<Feature>

Saves a new, empty draft and returns it. The draft has an id, an empty name and no publishedBuildId. Open it with openRoughBuilder. The draft stays in Rough even if the user closes the Builder without publishing. The SDK has no way to list or delete drafts, so keep the ID if you want to reopen it.

import { createRoughFeature } from '@roughapp/feature'

const feature = await createRoughFeature({ client, surface })
console.log(feature.id) // Pass to openRoughBuilder or mount after publishing.

openRoughBuilder(options)

openRoughBuilder(options: {
  client: RoughClient
  surface: RoughSurfaceDefinition
  featureId: Feature['id']
  theme?: 'light' | 'dark'
}): Promise<RoughBuilderHandle> // EventTarget & { close: () => Promise<void> }

Opens the Builder over your page for a Feature that already exists: a new draft, or a published Feature to edit. theme is 'light' (the default) or 'dark'.

The returned handle is an EventTarget. It fires feature:published when the user publishes, with { feature, surfaceKey } as the event's detail. close() closes the Builder and is safe to call more than once. Users can also close it themselves.

import { openRoughBuilder } from '@roughapp/feature'
import type { FeaturePublishedDetail } from '@roughapp/feature'

const builder = await openRoughBuilder({ client, surface, featureId: feature.id })
builder.addEventListener('feature:published', (event) => {
  console.log((event as CustomEvent<FeaturePublishedDetail>).detail)
})
// Later, if your page needs to close it:
await builder.close()

Types

Import these with import type. They exist only at compile time.

  • RoughClient: the type of the client returned by startRoughClient. Its only public member is destroy(); use SDK functions for other operations.
  • StartRoughClientOptions: the options for startRoughClient.
  • FetchUserTokenFn: () => string | Promise<string>
  • RoughSurfaceDefinition: the value from defineRoughSurface.
  • Feature, RoughFeatureMetadata: { id, name, publishedBuildId }
  • FeatureId, FeatureBuildId: branded strings. A plain string, for example one read from storage, needs a cast before you pass it to openRoughBuilder.
  • RoughFeatureMount: the value from mountRoughFeature.
  • RoughFeatureSubscription, SubscribeToRoughFeaturesOptions: the value from, and options for, subscribeToRoughFeatures.
  • CreateRoughFeatureOptions: the options for createRoughFeature.
  • RoughBuilderHandle, OpenRoughBuilderOptions: the value from, and options for, openRoughBuilder.
  • FeaturePublishedDetail: { feature, surfaceKey }, the detail of a feature:published event.
  • RoughFrameDatastore, JsonValue: the datastore option of mountRoughFeature, and the state it stores.
  • RoughTheme: 'light' | 'dark'
  • RoughCleanup: () => Promise<void>, the type of unmount, unsubscribe and close.