# 06 · Your own plugin

Turning on catalog plugins is great, but at some point you want a capability that only exists in your product. The incubator is the way there: an app-local plugin that follows the **same contract** as the official plugins. It's the step that turns you from an SDK user into an author.

## Generate the plugin

```bash
npx @fayz-ai/cli create plugin loyalty
```

```
✓ Created app-local plugin "src/plugins/loyalty"

  Add createLoyaltyPlugin() to your app's plugins array (see src/plugins/loyalty/README.md).
  Run "fayz doctor" to check boundaries.
```

What got created in `src/plugins/loyalty/`:

| File | Role |
| --- | --- |
| `index.ts` | The `PluginManifest`: id, navigation, routes and the page. The same contract as a `@fayz-ai/plugin-*`. |
| `data/types.ts` | The data contract (`LoyaltyDataProvider`) every backend implements. |
| `data/mock.ts` | Mock provider — the plugin already runs without a database. |
| `data/supabase.ts` | The real provider, going through the Fayz boundary (`getSupabaseClientOptional`), never importing the Supabase SDK directly. |
| `schema/index.ts` | Where the plugin's migrations live. |
| `README.md` | How to wire the plugin up, plus the graduation checklist for going official. |

## The contract is the same as the official ones

`index.ts` exports a factory that returns a `PluginManifest` — id, `navigation`, `routes` and a page. It picks the provider automatically (Supabase when there's a backend, mock otherwise) via `createSafeDataProvider`:

```ts
export function createLoyaltyPlugin(options?: LoyaltyPluginOptions): PluginManifest {
  const provider =
    options?.dataProvider ??
    createSafeDataProvider(
      () => createSupabaseLoyaltyProvider(),
      () => createMockLoyaltyProvider(),
    )
  // ...
  return {
    id: 'loyalty',
    name: 'Loyalty',
    navigation: [{ section: 'main', label: 'Loyalty', route: '/loyalty', icon: 'Puzzle' }],
    routes: [{ path: '/loyalty', component: Page }],
  }
}
```

To turn it on, add `createLoyaltyPlugin()` to the app config's plugins array (see the plugin's `README.md`). Then check the boundaries with doctor:

```bash
npx @fayz-ai/cli doctor
```

{% callout type="tip" %}
✓ You should see: the `src/plugins/loyalty/` folder with its six files, and `doctor` reporting `0 error(s)`. `data/supabase.ts` already uses `getSupabaseClientOptional` — if you imported `@supabase/supabase-js` directly, doctor would flag the boundary violation.
{% /callout %}

## Where the migrations go

Your plugin's tables live in `schema/index.ts` and are referenced from the manifest via `migrations: [...]`:

```ts
migrations: [{ id: 'loyalty-0001', version: '0.1.0', sql: '<create table ...>' }]
```

When you run `fayz db apply`, the `incubator` step (last in the order) applies those migrations along with everything else — which is exactly why the `spine → drizzle → seed → plugins → incubator` order matters.

## From incubator to official

Promoting an app-local plugin to an official `@fayz-ai/plugin-*` package is **packaging, not a rewrite**: the manifest doesn't change, you just move the folder. The generated `README.md` carries the full graduation checklist (validated contract, capability test, permissions, i18n, clean boundary). The same contract you followed here is the one the catalog plugins follow.

---

[← Previous: 05 · Real data with Supabase](/en/docs/tutorial/05-real-data-with-supabase) · [Next: 07 · Publish →](/en/docs/tutorial/07-publish)
