Files
paseo/docs/plugins.md
T
Mohamed Boudra 1c41ba0bee feat(plugins): add local plugin surfaces and RPCs
Keep plugin execution scoped to its daemon while allowing one local TypeScript entry point to contribute validated RPCs and native client surfaces. Paseo owns host selection, navigation chrome, and per-installation query caches.
2026-08-09 01:53:55 +02:00

2.7 KiB

Local plugins

Local plugins contribute daemon RPCs and native app surfaces from one index.tsx. Paseo executes the server contribution in a subprocess and evaluates the client contribution in the app runtime. Plugin code is trusted code; this first slice does not sandbox it.

Configure a directory source

Add the plugin to the root plugins object in $PASEO_HOME/config.json:

{
  "plugins": {
    "my-plugin": {
      "source": "directory",
      "path": "/absolute/path/to/my-plugin"
    }
  }
}

The directory contains an identity-only manifest and one entry point:

my-plugin/
  paseo-plugin.json
  index.tsx
{
  "id": "my-plugin"
}

The manifest ID must match the config key. Restart the development daemon after changing plugin source or configuration; directory watching is outside the first slice.

Contribute behavior and UI

Default export one contribution function. Paseo calls it with a plugin-scoped context. The compiler removes UI registrations from the server bundle and RPC registrations from the client bundle before resolving dependencies.

import { Text } from "react-native";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { defineRpc, type PluginContext, useRpc } from "@paseo/plugin";

const greetRpc = defineRpc({
  name: "greet",
  input: z.object({ name: z.string() }),
  output: z.object({ message: z.string() }),
});

function Greeting() {
  const greet = useRpc(greetRpc);
  const query = useQuery({
    queryKey: ["greeting", "Paseo"],
    queryFn: () => greet({ name: "Paseo" }),
  });
  return <Text>{query.data?.message}</Text>;
}

export default function contribute(plugin: PluginContext) {
  plugin.handle(greetRpc, async ({ name }) => ({ message: `Hello, ${name}` }));
  plugin.addSurface("main", Greeting);
  plugin.addSidebarItem({
    id: "main",
    title: "Greeting",
    icon: "MessageCircle",
    surface: "main",
  });
}

Paseo owns the route, screen header, Lucide icon validation, close action, theme DTO, layout facts, and render error boundary. The contributed component owns the complete body below the header.

RPC contracts validate inputs and outputs in both the app and plugin subprocess. useRpc returns a typed async function. Use the host-provided @tanstack/react-query for request state and caching; Paseo gives each plugin installation its own query client.

When the same plugin contribution exists on multiple hosts, Paseo shows it once in the sidebar and adds a host picker to the screen header. The selected host supplies the bundle, RPC transport, and query cache. Plugin code cannot address another host.

See examples/local-plugin for a runnable plugin.