Reference
SDK Reference
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
buildIdto 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 abuildIdto pin one version; an explicitly selected build can still be displayed after the Feature is unpublished. isEditabledefaults totrue, which shows an edit button when the user hovers over the Feature. Set it tofalseto hide the button. This is not a permission check.onfeaturepublishedis 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
datastorewithget(),set(state)andaddListener(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 bystartRoughClient. Its only public member isdestroy(); use SDK functions for other operations.StartRoughClientOptions: the options forstartRoughClient.FetchUserTokenFn:() => string | Promise<string>RoughSurfaceDefinition: the value fromdefineRoughSurface.Feature,RoughFeatureMetadata:{ id, name, publishedBuildId }FeatureId,FeatureBuildId: branded strings. A plainstring, for example one read from storage, needs a cast before you pass it toopenRoughBuilder.RoughFeatureMount: the value frommountRoughFeature.RoughFeatureSubscription,SubscribeToRoughFeaturesOptions: the value from, and options for,subscribeToRoughFeatures.CreateRoughFeatureOptions: the options forcreateRoughFeature.RoughBuilderHandle,OpenRoughBuilderOptions: the value from, and options for,openRoughBuilder.FeaturePublishedDetail:{ feature, surfaceKey }, the detail of afeature:publishedevent.RoughFrameDatastore,JsonValue: thedatastoreoption ofmountRoughFeature, and the state it stores.RoughTheme:'light' | 'dark'RoughCleanup:() => Promise<void>, the type ofunmount,unsubscribeandclose.