Architecture
How RAF works
RAF — the Rough Application Framework — is the architectural pattern that lets a host product safely embed user-built features. It's three layers, and a small, well-defined contract between them.
Everything in Rough rests on a single pattern. We call it RAF: the Rough Application Framework. It's the contract that lets a SaaS product expose a piece of itself for extension, while keeping the rest of the product safe from whatever the extension does.
There are three pieces to it: a Surface the host opens up, a tool list that defines what the surface allows, and a Feature that runs inside the surface and uses those tools.
The three layers
1. The Surface
A Surface is a region of the host product that the vendor has chosen to
open up to Rough. In practice, that means embedding the
RoughSurface component from the @roughapp/feature SDK
somewhere in your app — a panel, a sidebar, a section of a page — and
deciding what data and capabilities to pass into it.
Surfaces are the unit of permission. If you don't put a Surface in a part of your product, no Rough Feature can ever appear there. If you do, the Surface controls what's reachable from inside.
2. The tool list
Each Surface is initialised with a list of tools. A tool is a typed,
schema-described action that the Feature is allowed to call. There are three
kinds, exposed by @roughapp/bridge:
Query— read something from the host.Mutation— change something in the host.Subscription— listen for ongoing changes from the host.
Each tool defines a name, an input schema, an output schema, and an implementation. The schemas are written in Zod, which means inputs and outputs are both type-checked at compile time and validated at runtime.
The tool list is the contract. If a tool isn't in the list, the
Feature can't call it. There's no escape hatch, no eval, no
privileged channel — only the tools the host explicitly registered.
3. The Feature
A Rough Feature is the user-built thing that actually runs inside the Surface. It renders UI, reads from the tools the host provided, and calls mutations when it wants to make changes.
Features run inside an isolated runtime — an iframe with a postMessage bridge to the host — so a misbehaving Feature can only affect its own Surface. It doesn't share memory, DOM access or network identity with the host page.
How the pieces fit together
The shape of a host integration is small. Conceptually:
import { initRough, RoughSurface } from '@roughapp/feature'
import { Query, Mutation } from '@roughapp/bridge'
import { z } from 'zod'
// 1. Configure the SDK once.
initRough({
apiKey: '...',
projectId: '...',
})
// 2. Define the contract — what this Surface can read and do.
const getCurrentUser = new Query({
name: 'getCurrentUser',
inputSchema: z.unknown(),
outputSchema: z.object({ id: z.string(), name: z.string() }),
outputSample: { id: '01...', name: 'Jane' },
implementation: async () => loadCurrentUser(),
})
const createTask = new Mutation({
name: 'createTask',
inputSchema: z.object({ title: z.string() }),
outputSchema: z.object({ id: z.string() }),
outputSample: { id: '01...' },
implementation: async ({ title }) => createTaskInHost(title),
})
const toolList = [getCurrentUser, createTask] as const
// 3. Mount a Surface anywhere in your app.
// <RoughSurface surfaceId="dashboard-extras" toolList={toolList} context={...} /> With those three pieces in place, anything a user builds inside that Surface can only read the current user and create tasks. Nothing else is reachable, because nothing else exists from the Feature's point of view.
Why it's structured this way
Schema-defined tools mean type-safe extensions
Because every tool declares its input and output schema, Rough can generate TypeScript types and mocks for the Feature side automatically. The same schemas are used at runtime to validate what flows across the bridge in both directions, so the host can't be tricked into accepting malformed input from a Feature.
Process isolation gives you a hard boundary
The Feature runtime lives inside an iframe with a defined target origin
and a typed RPC channel (@roughapp/iframe). The host registers
each frame, the iframe connects with a matching channel and frame ID, and
every call after that is mediated by the bridge. The Feature can't see
cookies, local storage or DOM from the host page — it only sees what the
bridge gives it.
The host is always in charge
Tools are implemented in your code, not in Rough. When a Feature calls
createTask, your createTask function runs, in your
process, with your auth and your validation. Rough's job is just to be the
safe transport between the Feature and your code.
What this means in practice
- Adding a Surface is small. You decide the location and the tool list, and the SDK does the rest.
- Removing a tool is safe. If you stop registering a tool, Features that depended on it will no longer be able to call it. Nothing sneaks around the tool list.
- Auditing is straightforward. The complete list of
capabilities exposed to Rough is the union of every
toolListin your codebase. There's no other way for a Feature to reach you.
Related
- Features — what you build on top of RAF.
- Security — how RAF's boundaries are enforced.
- For engineering — what integrating RAF actually looks like.