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:

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

Related