> ## Documentation Index
> Fetch the complete documentation index at: https://docsv4.mile.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Building an Add-on

> Register pages, use the app's components, read and save data, add a one-time setup and follow the rules every add-on must keep.

You write in two places only:

* `src/plugin-entry.ts`: the list of your add-on's pages.
* `src/views/`: the pages themselves.

Leave everything else as it is, so your branch can take later SDK fixes.

## Register your pages

```ts src/plugin-entry.ts theme={null}
import './styles.css';
import { definePlugin } from './sdk';
import { OrdersPage } from './views/OrdersPage';
import { SettingsPage } from './views/SettingsPage';

export default definePlugin('Delivery Summary', [
  { component: OrdersPage, label: 'Orders', path: 'orders' },
  { component: SettingsPage, label: 'Settings', path: 'settings' },
]);
```

| Field | Meaning |
| - | - |
| `label` | The page's name in the left menu |
| `path` | The end of the page address: `/addons/<address>/orders` |
| `component` | The page |

Every page appears in the left menu under one entry with your add-on's name. An add-on with one page is a single entry.

## Build a page

Pages use the app's own components from `@mile/ui`, so your add-on looks like the rest of the app in light and dark mode.

```tsx src/views/OrdersPage.tsx theme={null}
import { useEffect, useState } from 'react';
import { Page, PageHeader, Button, EmptyState, Skeleton, notify } from '@mile/ui';
import { usePluginHost } from '../sdk';

export function OrdersPage() {
  const { http, user } = usePluginHost();
  const [flows, setFlows] = useState<{ _id: string; name: string }[] | null>(null);

  useEffect(() => {
    http
      .get('/flows', { params: { fields: '_id,name', limit: -1 } })
      .then((r) => setFlows(r.data?.flows?.data ?? []))
      .catch(() => notify.error('Could not load flows'));
  }, [http]);

  if (flows === null) return <Skeleton className="m-6 h-64" />;

  return (
    <Page>
      <PageHeader
        title="Orders"
        description={`Signed in as ${user?.name ?? '-'}`}
        actions={<Button onClick={() => notify.success('Saved')}>Save</Button>}
      />
      {flows.length === 0 ? (
        <EmptyState title="No flows yet" />
      ) : (
        <ul className="space-y-2">
          {flows.map((f) => <li key={f._id}>{f.name}</li>)}
        </ul>
      )}
    </Page>
  );
}
```

> [!NOTE]
>
> A text `title` on `PageHeader` is shown in the app's breadcrumb at the top, not inside the page. In the local preview, which has no breadcrumb, you won't see it.

### Components you can use

| Group | Components |
| - | - |
| Layout | `Page`, `PageHeader`, `PageToolbar`, `EmptyState`, `Card`, `Tabs`, `Sheet`, `Separator` |
| Inputs | `Button`, `Input`, `Textarea`, `Select`, `Checkbox`, `Switch`, `RadioGroup`, `Combobox`, `MultiCombobox`, `SearchInput`, `Calendar` |
| Dialogs | `Dialog`, `AlertDialog`, `ConfirmDialog`, `Popover`, `Tooltip`, `DropdownMenu` |
| Feedback | `notify.success`, `notify.error`, `Skeleton`, `Badge`, `Alert`, `Progress`, `InfoNote` |
| Data | `Pager`, `TruncatedText`, `ChartBar`, `ChartLine`, `ChartDonut` |
| Icons | About 210 [Lucide](https://lucide.dev/icons) icons, for example `FileText`, `Plus`, `Trash2`. For an icon outside that set, run `pnpm add lucide-react` and import it from `lucide-react` |

Style your own markup with Tailwind classes the app already uses, and prefer theme colours such as `bg-background`, `text-foreground` and `text-muted-foreground` so dark mode works.

## What your page receives

Call `usePluginHost()` anywhere inside a page:

| Field | What it is |
| - | - |
| `http` | The app's API client, already signed in as the current user |
| `user` | Name, email, organization and role of the signed-in user |
| `can(permission)` | Whether the user holds a permission, for example `can('view/task')` |
| `language` | The user's language: `en`, `id`, `tl`, `vi`, `th`, `zh`, `jp` or `ko` |
| `theme` | `light` or `dark` |
| `customModule` | This installation of your add-on |

`http` calls the same API documented in the [API Reference](https://docsv4.mile.app/api-reference/introduction.md). Write paths without the `/api/v3` prefix: `http.get('/tasks', { params: { flowId } })`.

## Save settings

`configApi` stores your add-on's settings for the organization, under the name the organization gave the add-on. They stay when you upload a new version, or remove and re-add the add-on under the same name.

> [!WARNING]
>
> If an organization renames the add-on under **Configure**, the add-on reads its settings under the new name and finds none, so setup runs again. Renaming back brings the settings back. Ask users to pick the name once.

```ts theme={null}
import { configApi } from '../sdk';

const settings = await configApi.load();               // null until the first save
await configApi.patch({ defaultFlowId: flowId });      // merge and save
```

## One-time setup and page permissions

An add-on with several pages usually creates one permission per page during a one-time setup, and checks it on every page.

Give each page permission the same `suffix` as the page's `path`, so the left menu and the page agree on who may open it.

```ts theme={null}
import { configApi, ensureCustomPermissions, checkPagePermission, getPermissionName } from '../sdk';

// Setup: create the permissions and grant them to the admin running setup
const permissionList = await ensureCustomPermissions(http, user, [
  { suffix: 'orders', displayName: 'View Orders' },
  { suffix: 'settings', displayName: 'View Settings' },
]);
await configApi.patch({ permissionList, setupComplete: true });

// Every page: is setup done, and may this user open it?
const ready = (await configApi.load())?.setupComplete === true;
const allowed = await checkPagePermission(http, user, getPermissionName('orders'));
```

PDF Generator follows this pattern with a three-step wizard. Its code is on the SDK's `add-on/pdf-generator` branch, including a ready-made setup wizard and translations in English and Indonesian that you can copy.

## Rules every add-on keeps

> **Use the app's components and React**
>
> Import components from `@mile/ui` and never install it. React comes from the app too. Do not change the build settings that keep React out of your file, and do not use your own router: the app decides which page to show.

> **Call the app only through http**
>
> Never create your own API client or read the sign-in token yourself. `http` already handles sign-in and expired sessions.

> **Do not call outside servers from the page**
>
> The app blocks any request from the page to a server that isn't MileApp. It works in the local preview, then fails after upload. To reach your own system, create a webhook automation so the app's servers call it.

> **Bundle your own libraries**
>
> Any library you install (a PDF, Excel or barcode library) is packed into your file. Two add-ons can use different versions of the same library.

> **Keep the stylesheet import in plugin-entry.ts**
>
> `import './styles.css'` must stay in `src/plugin-entry.ts`, or the uploaded add-on shows without styles.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.