Reference

SDK Reference

API reference for embedding Rough Features in your app.

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

This reference covers every public function, capability class and type in @roughapp/feature v0.8.0. Signatures are simplified to make them easier to read. For a step-by-step integration, see Getting Started. For release highlights, see the v0.8.0 release notes.

The SDK is ESM-only and runs only in the browser. Import it only in code that runs in the browser, not during server-side rendering. Use a modern browser and bundler; direct script-tag and CDN usage are not supported. Styles and the Builder's font are bundled, so there is no stylesheet to import.

npm install @roughapp/[email protected] zod@4

startRoughClient(options)

Starts a client and returns it immediately. Authentication and metadata synchronization run in the background. Use one client per Project, account and signed-in user, and create a new one when any of those changes.

startRoughClient(options: StartRoughClientOptions): RoughClient

type StartRoughClientOptions = {
  projectId: string
  externalUserId: string
  externalAccountId: string
  fetchUserToken: () => string | Promise<string>
  onError?: (error: Error) => void
  baseUrl?: string
  mediaBaseUrl?: string
}
Option Meaning
projectId Rough Project ID. Must match the identity token's iss.
externalUserId Your stable user ID. Must match the token's sub.
externalAccountId Your stable account ID. Must match the token's rough.account.id.
fetchUserToken Returns the identity token string from your authenticated backend, not a session object. Never keep signing keys in browser code.
onError Called when authentication fails, including token identity mismatches.
baseUrl Gateway URL; defaults to https://gateway.rough.app.
mediaBaseUrl CDN URL; defaults to https://cdn.rough.app.

Missing or invalid required options and invalid URLs throw a TypeError synchronously. Authentication errors go to onError; catch errors from other asynchronous SDK operations separately. See the token rules for server-side authentication requirements.

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()

Client lifetime

Pass the client returned by startRoughClient() to SDK operations. Its only public method is destroy(): Promise<void>. RoughClient is a type, not a constructor; the client's internal services are not part of the public API.

Unmount Features before destroying their client. destroy() closes the client's subscriptions and Builders and waits for their cleanup. It is safe to call more than once. In lifecycle hooks that cannot await cleanup, use void client.destroy(); tests and controlled transitions should await it.

mount.unmount()
await client.destroy()

Each Project, user and account combination has its own metadata cache. Feature application state is stored separately; see datastores.

defineRoughSurface(surface)

Defines a Surface: a place in your app where Features can appear, and the capabilities they can use there.

defineRoughSurface<Key extends string>(surface: RoughSurface<Key>): RoughSurface<Key>

type RoughSurface<Key extends string = string> = Readonly<{
  key: Key
  name: string
  description: string
  toolList: readonly (Query | Mutation | Subscription)[]
}>

key identifies the Surface. name and description describe it to Rough; toolList contains the capabilities implemented by your app.

Returns the object you pass in, without validating, copying or freezing it. Keep the same object while it is in use, and leave both the definition and its capability list unchanged. Use consistent definitions for a given key. Changing the key selects a different Surface; changing or removing capabilities can break existing Features.

Definitions have no client or Project state and can be shared across clients. Creating a Feature or opening its Builder registers the Surface with Rough. Subscribing to or mounting existing Features does not register it.

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

Each capability takes an id, name, description, Zod 4 inputSchema and outputSchema, an outputSample, and an implementation.

The implementation runs in your app, so check permissions there as you would for any other request. Schemas validate data, not permissions. Rough receives the serialized schemas and samples, not your implementation code. Builder previews use samples rather than live implementations, so use representative made-up data, not customer information or secrets.

// 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>
Capability Implementation
new Query(options) Read once: async (parsedInput) => output.
new Mutation(options) Write once: async (parsedInput) => output.
new Subscription(options) Ongoing updates: async (emit, parsedInput) => unsubscribe. Emit each new output and return a function that stops updates.

Inputs are parsed before calling the implementation; returned or emitted outputs are validated and parsed before being passed to the Feature. Invalid values throw an Error. Implementations receive z.output<InputSchema> and produce z.input<OutputSchema>, so schema defaults and transformations matter.

Query.create(), Mutation.create() and Subscription.create() are alternatives to the constructors. Instances also expose query.call(input), mutation.call(input) and subscription.subscribe(emit, input) for validated calls from host code. call() returns a promise for parsed output; subscribe() returns a promise for an unsubscribe function.

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)

Reports which Features are published on a Surface and calls onChange when that list changes.

subscribeToRoughFeatures(options: SubscribeToRoughFeaturesOptions): () => void

type SubscribeToRoughFeaturesOptions = {
  client: RoughClient
  surface: RoughSurface
  onChange: (features: RoughFeature[]) => void
}

The list is sorted by ID in descending order. onChange runs immediately, before subscribeToRoughFeatures() returns, with the current local list. It runs again when that list changes. The first list may be empty while metadata is loading, so an empty list does not tell you whether loading has finished.

Each item has id, name, surfaceKey and publishedFeatureBuildId. Drafts and unpublished Features are excluded. Unpublishing removes a Feature from the list. Name and published-build changes also trigger an update; unrelated build progress does not.

Call the returned unsubscribe function when the owning view is removed. It returns void and is safe to call more than once. The subscription has no ready promise or onError callback; handle authentication errors through the client's onError callback.

import { subscribeToRoughFeatures } from '@roughapp/feature'

const unsubscribe = subscribeToRoughFeatures({
  client,
  surface,
  onChange(features) {
    // Reconcile your rendered Features with this list.
    console.log(features)
  },
})

// Register with your view's teardown; do not call immediately after subscribing.
function cleanupSubscription() {
  unsubscribe()
}

mountRoughFeature(container, props)

Renders a Feature inside an element you own, using a shadow root.

mountRoughFeature(container: HTMLElement, props: {
  client: RoughClient
  surface: RoughSurface
  featureId: FeatureId
  buildId?: FeatureBuildId
  datastore?: RoughFrameDatastore
  isEditable?: boolean
}): RoughFeatureMount

type RoughFeatureMount = {
  update: (partialProps) => void
  unmount: () => void
}
  • Omit buildId to follow the currently published build. New publications update the mounted Feature without remounting it. Unpublishing stops displaying that build and shows an unpublished status message.
  • Pass buildId to display a specific build, even after unpublishing. Its metadata must be available to the client.
  • isEditable defaults to true. Set it to false to hide the edit control. Your app must still enforce permissions.
  • datastore overrides the default application-state persistence described below.

update() accepts a partial set of mount props. Use update({ buildId: undefined }) to resume following the published build. Call unmount() before removing the container. It returns void and is safe to call more than once.

Observe publication changes with subscribeToRoughFeatures().

import { mountRoughFeature } from '@roughapp/feature'

const container = document.querySelector<HTMLElement>('#feature-slot')!
const mount = mountRoughFeature(container, {
  client,
  surface,
  featureId: feature.id,
})
mount.update({ isEditable: false })

// Register with the owning view's teardown.
function cleanupFeature() {
  mount.unmount()
  container.remove()
}

Feature state and RoughFrameDatastore

By default, application state is saved in the browser's local storage, keyed only by Feature ID—not by user or account. People opening the same Feature in the same browser origin share that stored state, even after switching users. This is separate from synchronized Feature metadata.

For user/account isolation, shared state across browsers, or your own persistence, pass a datastore to mountRoughFeature().

type RoughFrameDatastore = {
  get: () => JsonValue | undefined
  set: (state: JsonValue) => void | Promise<void>
  addListener: (callback: (state: JsonValue) => void) => () => void
}

get() returns the current JSON state, or undefined if none exists. set() saves state and should notify registered listeners. Notify listeners for changes received from other viewers too. addListener() returns a function that removes that listener. Keep the same datastore object while the Feature is mounted, and use separate state per Feature and, when needed, per user/account.

The SDK does not await the promise returned by set(); handle persistence failures inside your implementation.

import type { JsonValue, RoughFrameDatastore } from '@roughapp/feature'

// Illustrative in-memory store; replace with your persistence layer.
let state: JsonValue | undefined
const listeners = new Set<(state: JsonValue) => void>()
const datastore: RoughFrameDatastore = {
  get: () => state,
  set(next) {
    state = next
    for (const listener of listeners) listener(next)
  },
  addListener(callback) {
    listeners.add(callback)
    return () => {
      listeners.delete(callback)
    }
  },
}

createRoughFeature(options)

Creates an unpublished Feature that you can open in the Builder.

createRoughFeature(options: CreateRoughFeatureOptions): Promise<RoughFeature>

type CreateRoughFeatureOptions = {
  client: RoughClient
  surface: RoughSurface
  name?: string
}

The Feature is created locally and saved to Rough in the background. Once the promise resolves, you can pass its ID to openRoughBuilder(); you don't need to wait for synchronization. The resolved promise does not confirm that Rough has saved it on the server.

name defaults to an empty string; surfaceKey is the Surface's key and publishedFeatureBuildId is undefined. Closing the Builder without publishing keeps the draft. Keep its ID if you want to reopen it: the public SDK has no API to list or delete drafts.

import { createRoughFeature } from '@roughapp/feature'

const feature = await createRoughFeature({
  client,
  surface,
  name: 'Inbox summary',
})

openRoughBuilder(options)

Opens the Builder over your page to edit an existing Feature, whether it's a draft or already published. Create the Feature first if needed.

openRoughBuilder(options: OpenRoughBuilderOptions): Promise<RoughBuilderHandle>

type OpenRoughBuilderOptions = {
  client: RoughClient
  surface: RoughSurface
  featureId: FeatureId
  theme?: RoughTheme
}

type RoughBuilderHandle = { close: () => void }

theme defaults to 'light'; pass 'dark' for the dark Builder UI. It does not change the theme of the generated Feature iframe.

Call the returned handle's close() method to close the Builder. It returns void and is safe to call more than once. Users can also close the Builder from its UI.

Observe publication changes with subscribeToRoughFeatures(). Publishing updates the local list immediately, before Rough confirms the change on the server.

import { openRoughBuilder } from '@roughapp/feature'

const builder = await openRoughBuilder({
  client,
  surface,
  featureId: feature.id,
  theme: 'dark',
})

// Later, when the host needs to close it:
builder.close()

Types

Use import type for these exports.

Type Meaning
RoughClient Client handle with destroy(): Promise<void>.
StartRoughClientOptions Client startup options.
FetchUserTokenFn () => string | Promise<string>.
RoughSurface<Key = string> Readonly Surface definition with an optional string key type parameter.
RoughFeature Metadata returned by creation and subscription calls; shape below.
FeatureId, FeatureBuildId Branded string IDs.
RoughFeatureMount Mount handle with update() and unmount().
SubscribeToRoughFeaturesOptions Subscription options; the result is a plain cleanup function.
CreateRoughFeatureOptions Feature creation options, including optional name.
OpenRoughBuilderOptions, RoughBuilderHandle Builder options and its close-only handle.
RoughFrameDatastore, JsonValue Application-state persistence interface and JSON value type.
RoughTheme 'light' | 'dark'.
type RoughFeature = {
  id: FeatureId
  name: string
  surfaceKey: string
  publishedFeatureBuildId: FeatureBuildId | undefined
}

type JsonValue =
  | null
  | boolean
  | number
  | string
  | JsonValue[]
  | { [key: string]: JsonValue }

IDs returned by SDK calls can be passed directly to other SDK functions. A plain string read from your own storage or a URL needs a cast to FeatureId or FeatureBuildId after you validate it. These brands are compile-time only; a cast is not a runtime existence or authorization check.