Skip to content

Why PLEX?

Most apps have a fixed core that the app’s team writes. Code from anyone else gets only the hooks the team chose to expose, and two such add-ons rarely work with each other. A PLEX app is the plugins it loads. The navigation tree, the settings page and the assistant are plugins too, so a plugin from outside the team uses the same API as a built-in one.

A plugin says which object type it renders and where. The app asks for “the article for this object” and renders whatever plugin answers, without importing it.

import * as
import Surface
Surface
from '@dxos/app-framework/Surface';
import * as
import AppSurface
AppSurface
from '@dxos/app-toolkit/AppSurface';
export const
const NoteArticleSurface: ReactDefinition<T>
NoteArticleSurface
=
import Surface
Surface
.
const create: <Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}, {
role: string;
subject: Note & OfKind<EntityKind.Object>;
attendableId: string;
}, "article">(definition: Readonly<{
id: "article";
filter: Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}>;
position?: Position;
component: React.ComponentType<...>;
props?: ((props: ComponentProps<...>) => {
...;
}) | undefined;
}>) => ReactDefinition<Omit<...> & {
subject: Note & OfKind<EntityKind.Object>;
}>

Creates a React surface definition from a typed filter.

create
({
id: "article"
id
: 'article',
// Matches the article role when the data's subject is a Note.
filter: Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}>
filter
:
import AppSurface
AppSurface
.
const object: <Role<AppSurface.ArticleData<any, {}, unknown>>, typeof Note>(token: Role<AppSurface.ArticleData<any, {}, unknown>>, schema: typeof Note, predicate?: ((data: NonNullable<AppSurface.ArticleData<any, {}, unknown>>) => boolean) | undefined) => Surface.Filter<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}> (+1 overload)
object
(
import AppSurface
AppSurface
.
const Article: Role<AppSurface.ArticleData<any, {}, unknown>>

Role token for the article role. Subject defaults to any until narrowed via

object

,

literal

, or

subject

. Extra passthrough props are permitted.

Article
,
class Note
Note
),
component: React.ComponentType<{
role: string;
subject: Note & OfKind<EntityKind.Object>;
attendableId: string;
}>

Accepts any component type (not just a function) so a container re-exported through a lazy() barrel as ComponentType<any> can be registered without a cast.

component
:
const NoteArticle: ({ role, subject, }: AppSurface.ObjectArticleProps<Note>) => React.JSX.Element
NoteArticle
,
// Maps the surface's data onto the component's props; checked against the
// filter's data type.
props?: ((props: ComponentProps<Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}>) => {
role: string;
subject: Note & OfKind<EntityKind.Object>;
attendableId: string;
}) | undefined

Maps the surface props onto the component's own props, so a plain container can be registered directly instead of being wrapped in an adapter that unpacks data.

Prefer this over an inline component: ({ data }) => <Container … />: the mapper's input type derives from the same filter that defines the surface's data shape, so the unpacking is checked against the filter rather than restated by hand.

@example Surface.create({ id: 'defaultPluginSettings', filter: AppSurface.settings(AppSurface.Article), component: DefaultSettings, props: ({ data: { subject } }) => ({ subject }), });

props
: ({
role: string

The resolved role NSID string (e.g. org.dxos.role.article). Present on matched components only — multi-role containers can branch on this value by comparing against a token's .role (e.g. role === AppSurface.Section.role).

role
,
data: Omit<NonNullable<AppSurface.ArticleData<any, {}, unknown>>, "subject"> & {
subject: Note & OfKind<EntityKind.Object>;
}
data
: {
subject: Note & OfKind<EntityKind.Object>
subject
,
attendableId: string
attendableId
} }) => ({
role: string
role
,
subject: Note & OfKind<EntityKind.Object>
subject
,
attendableId: string
attendableId
,
}),
});
App with a fixed core PLEX app
What the core is Code the app’s team writes The plugins it loads, including the core UI
What outside code can change Only the hooks the team exposes Anything a built-in plugin can, through the same capabilities
How two add-ons work together Through the host, if it allows it One plugin reads another’s capabilities and renders its surfaces
When outside code loads Usually at startup Each module when its activation event fires
How an agent acts A separate API, if there is one Through the same operations the UI runs
Where outside code keeps its data A store of its own ECHO objects in spaces, which every plugin can query

Plugins extend each other through capabilities: typed slots that one module contributes to and another reads. A plugin can render inside another plugin’s view by contributing a surface for a role, or add an action to another plugin’s objects by adding nodes to App Graph. Neither plugin imports the other. Surfaces and App Graph explain both.

An app with many plugins cannot load all of them at startup. Each module waits for an activation event, such as the first time a view is needed. A surface module loads the first time one of its roles is requested, and an operation handler loads the first time the operation runs. ActivationEvent lists the events.

An operation is defined once, with a typed input and output. A button invokes it, the command palette lists it, and the assistant calls it as a tool, reading the schema to fill in the input. None of the three needs code of its own. Operations explains how.

A plugin’s data is ECHO objects in spaces. It syncs between a user’s devices and the people they share with, merges concurrent edits and works offline, with no sync code in the plugin. Spaces explains the model.

PLEX does not store or sync data; ECHO does. It is not a component library either: plugins build their views with @dxos/react-ui, or with any React components.

Start with Create a plugin.