Reference
SDK Reference
API reference for embedding Rough Features in your app.
This reference covers every public function, capability class and type
in @roughapp/feature v0.9.0. Signatures are simplified to make them easier
to read. For a step-by-step integration, see Getting Started.
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>
theme?: RoughTheme
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. |
theme |
Builder UI theme: 'light' (default) or 'dark'. Does not theme generated Features. |
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,
theme: 'light',
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),
})
// Follow changes to your app's theme without recreating the client.
client.setTheme('dark')
Client lifetime
Pass the client returned by startRoughClient() to SDK operations. Its public
methods are setTheme(theme: RoughTheme): void and destroy(): Promise<void>.
RoughClient is a type, not a constructor; the client's internal services are
not part of the public API.
Each client owns one Builder UI, including a minimized launcher when closed.
Opening another Feature navigates that same Builder rather than creating a
separate instance. setTheme() updates its theme, including the minimized UI;
it does not change generated Feature themes.
Unmount Features before destroying their client. destroy() closes the
client's subscriptions, removes its Builder UI 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
buildIdto 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
buildIdto display a specific build, even after unpublishing. Its metadata must be available to the client. isEditabledefaults totrue. Set it tofalseto hide the edit control. Your app must still enforce permissions.datastoreoverrides 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<void>
type OpenRoughBuilderOptions = {
client: RoughClient
surface: RoughSurface
featureId: FeatureId
}
Registers the Surface, navigates the client's existing Builder to the Feature and opens it. The promise resolves without a return value; there is no Builder handle or public close method. Users close the Builder from its UI, leaving the minimized launcher available to reopen it. Closing does not delete drafts or cancel builds.
Set the Builder theme with the theme option to startRoughClient() or
client.setTheme('dark'), not with an option to openRoughBuilder().
Observe publication changes with subscribeToRoughFeatures(). Publishing
updates the local list immediately, before Rough confirms the change on the
server.
import { openRoughBuilder } from '@roughapp/feature'
await openRoughBuilder({
client,
surface,
featureId: feature.id,
})
Types
Use import type for these exports.
| Type | Meaning |
|---|---|
RoughClient |
Client handle with setTheme(theme: RoughTheme): void and 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 |
Options for opening the client's Builder for a Feature. |
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.