# Incubator

The incubator is where plugins are born: a folder inside your app that follows the **same contract** as the official plugins. It's the path to a capability that only exists in your product — and 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 appears 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`) that 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. |

## 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',
    icon: 'Puzzle',
    version: '0.1.0',
    navigation: [{ section: 'main', position: 50, label: 'Loyalty', route: '/loyalty', icon: 'Puzzle' }],
    routes: [{ path: '/loyalty', component: Page }],
  }
}
```

To wire it up, add `createLoyaltyPlugin()` to your app config's plugins array (the generated `README.md` shows where). Then check the boundaries:

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

## The boundary is mandatory

The generated `data/supabase.ts` **never** imports `@supabase/supabase-js` directly — it gets the client from `getSupabaseClientOptional()`, the Fayz boundary, and scopes the query by `tenant_id`:

```ts
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'

// ...
const { data, error } = await sb()
  .from('loyalty')
  .select('*')
  .eq('tenant_id', getActiveTenantId())
```

If you swapped that for a direct import of the Supabase SDK, `fayz doctor` would flag the boundary break. That's the rule that keeps your plugin promotable — the details are in [Patterns](/en/docs/building-plugins/patterns).

## 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 ...>' }]
```

During `fayz db apply`, the `incubator` step (the last in the order) applies these migrations along with the rest — which is 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 rewriting**: the manifest doesn't change, you just move the folder into its own package. The generated `README.md` carries the full graduation checklist (contract validated, capability test, migrations wired, permissions declared, complete i18n, clean boundary). The same contract you followed here is the one the catalog plugins follow — there's nothing an official plugin can do that yours can't.

---

Next: every field a plugin can declare, in [Plugin manifest](/en/docs/building-plugins/manifest).
