Documentation

Plugin Development

Build custom tools for Bely using the @usebely/sdk.

Overview

Bely plugins are small React apps that run inside an isolated iframe. They can add new tools to the launcher, search providers, custom actions, and themes. Each plugin is a folder with:

  • bely.plugin.json — manifest with metadata and permissions
  • dist/index.js — the built JavaScript bundle
  • src/ — your source code (included in the bundle for review)

Quick Start

Create a new folder for your plugin and set up the project:

mkdir my-plugin && cd my-plugin
pnpm init
pnpm add @usebely/sdk react react-dom
pnpm add -D esbuild typescript @types/react

Plugin Manifest

Create `bely.plugin.json` at the root of your plugin:

{
  "name": "my-plugin",
  "displayName": "My Plugin",
  "version": "1.0.0",
  "description": "What your plugin does",
  "author": { "name": "Your Name" },
  "icon": "package",
  "category": "dev",
  "types": {
    "tool": {
      "mode": "my-plugin",
      "aliases": ["myp", "my"]
    }
  },
  "permissions": ["fetch"],
  "entry": "dist/index.js",
  "minBelyVersion": "1.26.0"
}
FieldDescription
nameUnique slug (lowercase, hyphens)
displayNameShown in the UI
iconAny Lucide icon name (e.g. package, code, globe)
categorydev design productivity utils system web3 other
types.tool.modeMode slug to open the plugin
types.tool.aliasesAlternative search terms
permissionsAPIs the plugin needs access to
entryPath to the built JS bundle

Entry Point

Create `src/index.tsx` — this is your plugin's main file:

import { mount, List, useFetch } from "@usebely/sdk";
import { useState } from "react";

function App() {
  const [query, setQuery] = useState("");

  const { data, loading } = useFetch(
    query.length >= 2
      ? `https://api.example.com/search?q=${query}`
      : null
  );

  return (
    <List
      searchBarPlaceholder="Search..."
      onSearchChange={setQuery}
      isLoading={loading}
    >
      {!query.trim() ? (
        <EmptyState />
      ) : (
        data?.results?.map((item) => (
          <List.Item
            key={item.id}
            id={item.id}
            title={item.name}
            subtitle={item.description}
          />
        ))
      )}
    </List>
  );
}

function EmptyState() {
  return (
    <div style={{
      display: "flex", flexDirection: "column",
      alignItems: "center", padding: "30px 20px",
      color: "var(--glass-text-muted)",
    }}>
      <span style={{ fontSize: 12 }}>Type to search</span>
    </div>
  );
}

mount(<App />);

Build Script

Create `build.mjs` to bundle your plugin with esbuild:

import { build } from "esbuild";

await build({
  entryPoints: ["src/index.tsx"],
  bundle: true,
  format: "iife",
  outfile: "dist/index.js",
  jsx: "automatic",
  external: ["react", "react-dom"],
  minify: true,
});

console.log("Built dist/index.js");

Add scripts to your `package.json`:

{
  "scripts": {
    "build": "node build.mjs",
    "dev": "node build.mjs --watch"
  }
}
React and React-DOM are provided by the host app — don't bundle them. Use external in esbuild to exclude them.

SDK Components

The SDK provides components that automatically follow the Bely glass theme.

List

Searchable list with items, sections, and action panels.

<List
  searchBarPlaceholder="Search..."
  onSearchChange={(text) => {}}
  isLoading={false}
>
  <List.Item
    id="1"
    title="Item name"
    subtitle="Description"
    icon="P"
    accessories={[{ text: "v1.0" }]}
    actions={
      <ActionPanel>
        <Action title="Open" onAction={() => {}} />
      </ActionPanel>
    }
  />
  <List.Section title="Category">
    <List.Item id="2" title="Grouped item" />
  </List.Section>
</List>

Detail

Detail view with header, markdown, metadata rows, and actions.

<Detail
  title="Item Name"
  onBack={() => setSelected(null)}
  markdown="Description text here"
  metadata={[
    { label: "Version", value: "1.0.0" },
    { label: "License", value: "MIT" },
  ]}
  actions={
    <ActionPanel>
      <Action.CopyToClipboard content="npm install pkg" title="Copy" />
      <Action.OpenInBrowser url="https://example.com" title="Open" />
    </ActionPanel>
  }
/>

Form

Form with text fields, text areas, dropdowns, and checkboxes.

<Form onSubmit={(values) => console.log(values)}>
  <Form.TextField id="name" title="Name" placeholder="Enter name" required />
  <Form.TextArea id="desc" title="Description" />
  <Form.Dropdown id="type" title="Type" options={[
    { label: "Option A", value: "a" },
    { label: "Option B", value: "b" },
  ]} />
  <Form.Checkbox id="active" title="Active" defaultValue={true} />
</Form>

Grid

Grid layout for visual items.

<Grid columns={3} searchBarPlaceholder="Search...">
  <Grid.Item id="1" title="Item" subtitle="Info" icon="A" />
</Grid>

Action & ActionPanel

Inline action buttons with copy and open browser shortcuts.

<ActionPanel>
  <Action title="Run" onAction={() => {}} shortcut="Enter" />
  <Action.CopyToClipboard content="text" title="Copy" />
  <Action.OpenInBrowser url="https://..." title="Open" />
</ActionPanel>

SDK Hooks

Built-in hooks for common plugin operations.

useFetch

Proxied through Rust (no CORS). Built-in 300ms debounce.

const { data, loading, error, refetch } = useFetch<T>(
  url,                    // string | null (null = skip)
  { debounce: 500 }      // optional, default 300ms
);

useStorage

Sandboxed per-plugin localStorage.

const { value, setValue, remove } = useStorage<T>("key", defaultValue);

useClipboard

Requires clipboard.read or clipboard.write permission.

const { text, copy, paste } = useClipboard();
await copy("text to copy");

useNavigation

Navigate back within the plugin or exit to the host.

const { pop } = useNavigation();
pop(); // Go back to previous view

usePluginContext

Access plugin ID, query, theme, and locale.

const { pluginId, query, theme, locale } = usePluginContext();

useTranslation

Internationalize your plugin. Automatically uses the host locale (en, pt-BR, es).

const { t, locale } = useTranslation({
  en: { greeting: "Hello", search: "Search..." },
  "pt-BR": { greeting: "Olá", search: "Buscar..." },
  es: { greeting: "Hola", search: "Buscar..." },
});

// t("greeting") → "Olá" (if user locale is pt-BR)

useSystemInfo

Requires system.info permission.

const { os, platform, theme, locale, belyVersion } = useSystemInfo();

Internationalization

Bely supports 3 languages: English (en), Portuguese (pt-BR), and Spanish (es). Use the `useTranslation` hook to localize your plugin automatically based on the user's language.

import { mount, List, useTranslation } from "@usebely/sdk";

const translations = {
  en: {
    search: "Search packages...",
    name: "Name",
    version: "Version",
    noResults: "No results found",
  },
  "pt-BR": {
    search: "Buscar pacotes...",
    name: "Nome",
    version: "Versão",
    noResults: "Nenhum resultado encontrado",
  },
  es: {
    search: "Buscar paquetes...",
    name: "Nombre",
    version: "Versión",
    noResults: "No se encontraron resultados",
  },
};

function App() {
  const { t, locale } = useTranslation(translations);

  return (
    <List searchBarPlaceholder={t("search")}>
      <List.Item
        title={t("noResults")}
        subtitle={locale}
      />
    </List>
  );
}

mount(<App />);
The hook resolves the best locale match: exact (pt-BR), then base language (pt), then falls back to English. Missing keys also fall back to the English translation.

Permissions

Plugins run in a sandboxed iframe. Declare required permissions in the manifest:

"permissions": ["fetch", "clipboard.write"]
PermissionDescription
fetchMake HTTP requests (proxied through Rust, no CORS)
clipboard.readRead from clipboard
clipboard.writeWrite to clipboard
fs.readRead files from disk
fs.writeWrite files to disk
system.infoGet OS info, theme, locale
Storage (storage.get, storage.set) and URL opening (open.url) are always allowed without declaring permissions.

Theming

Your plugin automatically inherits the user's theme. The host injects 40+ CSS variables into the iframe.

/* Use CSS variables */
background: var(--glass-btn-bg);
border: 1px solid var(--glass-btn-border);
color: var(--glass-text);

/* Never hardcode colors */
background: #18181B;  /* DON'T DO THIS */
VariableUsage
--glass-bgBackground with transparency
--glass-textPrimary text
--glass-text-dimSecondary text
--glass-text-mutedTertiary/hint text
--glass-btn-bgButton/input background
--glass-btn-borderButton/input border
--glass-selectedHover/selected state
--glass-dividerDivider lines
--accentAccent color (user-customizable)
--accent-bgAccent background (translucent)
--accent-borderAccent border
SDK components use these variables automatically. Only use them directly if you write custom styles.

Testing Locally

To test your plugin during development:

  1. Build the plugin: pnpm build
  2. In Bely, go to Plugin Store > Installed tab
  3. Click "Install from local folder"
  4. Select your plugin folder
  5. The plugin appears in the toolbox
pnpm build
# Then hover the plugin in Installed > click the refresh icon

To update after code changes:

Publishing to the Store

To submit your plugin to the Bely Plugin Store:

  1. Make sure your plugin has bely.plugin.json, built dist/, and src/ with source code
  2. In Bely, go to Plugin Store > click Submit
  3. Fill in the metadata (name, description, icon, category, version)
  4. Select your plugin folder and click Submit for review
  5. Your plugin will be reviewed and approved or rejected with feedback
Bundle size limit: 5MB. Approved versions cannot be edited — submit a new version instead. Pending versions can be updated before approval.
Windows · Android · Early access

Join the

whitelist.

Bely is in private beta. Drop your email and we'll
let you in as soon as your spot opens up.

No spam. Unsubscribe anytime.