fayzfayz sdk
PTEN

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

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/:

FileRole
index.tsThe PluginManifest: id, navigation, routes and the page. The same contract as a @fayz-ai/plugin-*.
data/types.tsThe data contract (LoyaltyDataProvider) every backend implements.
data/mock.tsMock provider — the plugin already runs without a database.
data/supabase.tsThe real provider, going through the Fayz boundary (getSupabaseClientOptional), never importing the Supabase SDK directly.
schema/index.tsWhere the plugin's migrations live.
README.mdHow 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:

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:

npx @fayz-ai/cli doctor

✓ 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.

Where the migrations go

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

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 · Next: 07 · Publish →