Getting Started
Expose your data
React
Now we’ll give Rough access to some data from your app. Pick something your users already work with—customers, orders, support tickets, or whatever makes sense for your product.
We’re using books in the examples, but you don’t need to build a books API to follow along. Use your own model, names, and data-access functions in each of the files below. The important parts are the schema, the fictional sample, and the function that reads your real data.
Choose an existing data source
Start with a small, read-only dataset the signed-in user can already access. Your capability can call an existing API client, read a browser store, or use a query cache. There’s no special Rough endpoint to implement.
The implementation runs in the browser, inside your app. Don’t import a database client or server-only module into it. If the data currently exists only on your server, use your app’s normal authenticated data-access path to make it available to browser code.
Describe the data you want to expose
A schema tells Rough what a capability returns. Describe your actual output rather than reshaping your app’s data to match our books example. You can leave out fields a Feature doesn’t need.
import { z } from 'zod'
// Books illustrate the shape of a capability. Use your app's own model here.
export const statusSchema = z.enum(['want-to-read', 'reading', 'finished'])
export const bookSchema = z.object({
id: z.string(),
title: z.string(),
author: z.string(),
readingStatus: statusSchema,
})
export const booksSchema = z.array(bookSchema)
export type Book = z.infer<typeof bookSchema>
// Fictional values: these are sent to Rough for Builder previews.
export const sampleBooks: Book[] = [
{
id: 'example-1',
title: 'The Glass Orchard',
author: 'Alex Rowan',
readingStatus: 'reading',
},
{
id: 'example-2',
title: 'A Map of Quiet Places',
author: 'Sam Rivers',
readingStatus: 'want-to-read',
},
]
Replace the schema and sample together. For a tickets app, for example, you might expose a ticket’s ID, subject, priority, and status instead. Give optional and nullable fields the same meaning they have in your real response.
The Builder uses outputSample to preview Features before they’re published. Use made-up values, never customer data or secrets. Rough receives your schemas and samples, not the implementation code. Published Features call the implementation to access real data; the sample isn’t a fallback for a failing request.
Connect your existing read function
This adapter is the boundary between the example and your app. listBooks below represents an existing browser-safe function that reads the current account’s data. The import is illustrative, not a file supplied by Rough or a module you need to recreate. Replace it with your own API client or store access.
import { listBooks } from '@/lib/app/data' // Your app's existing data access.
import { booksSchema } from './data'
import type { Book } from './data'
export type DashboardData = {
read: () => Promise<Book[]>
}
export function createDashboardData(accountId: string): DashboardData {
return {
read: async () => {
const books = await listBooks({ accountId })
return booksSchema.parse(books)
},
}
}
If your API already derives the active account from the session, you may not need an account argument. If you do send an account ID, your server must verify the user has access to it. An ID supplied by the browser is not proof of membership.
Use your existing authenticated request helper, including error handling and credentials. Failed reads should reject, not quietly return an empty list. The explicit schema parse here catches a mismatch at your app boundary; the SDK also validates capability outputs.
Define a Query
A capability is an operation you let a Feature use. We’ll start with a Query, which reads data once.
import { Query } from '@roughapp/feature'
import { z } from 'zod'
import { booksSchema, sampleBooks } from './data'
import type { DashboardData } from './data-source'
export function booksQuery(data: DashboardData) {
return new Query({
id: 'getBooks',
name: 'Get books',
description:
'Returns books the signed-in user can access, including title, author and reading status.',
inputSchema: z.object({}),
outputSchema: booksSchema,
outputSample: sampleBooks,
implementation: () => data.read(),
})
}
Give your capability a name and description that explain what the data means. “Returns unresolved support tickets for the active organisation” tells the Builder more than “Fetches data.” Rename the ID and description to fit your app before creating Features with it.
Put it on a Surface
A Surface is a place in your app where Features can appear. Its capability list says what those Features can do. The SDK calls that list toolList.
import { defineRoughSurface } from '@roughapp/feature'
import { booksQuery } from './query'
import type { DashboardData } from './data-source'
export function createDashboardSurface(data: DashboardData) {
return defineRoughSurface({
key: 'dashboard',
name: 'Reading dashboard',
description: 'A place to organise books and track reading.',
toolList: [booksQuery(data)],
})
}
Choose a stable Surface key and a name that fits where you’re adding Rough. Once users have created Features, keep the key stable: changing it creates a different Surface, and the existing Features won’t appear there.
The factory receives the current account’s data adapter. In the next step we’ll create both when the page mounts, rather than storing one user’s data in a global server variable.
Check your read function
Call your adapter’s read() from your app’s browser code while signed in. It should return your real data and pass your schema. Check that a user can’t read another account’s data by changing an ID in a request.
There’s no books endpoint to test unless your product really is about books. What matters is that your chosen data source works through your app’s existing permissions.