Incubator
O incubator é onde os plugins nascem: uma pasta dentro do seu app que segue o mesmo contrato dos plugins oficiais. É o caminho para uma capacidade que só existe no seu produto — e é o passo que transforma você de usuário do SDK em autor.
Gere o plugin
npx @fayz-ai/cli create plugin fidelidade
✓ Created app-local plugin "src/plugins/fidelidade" Add createFidelidadePlugin() to your app's plugins array (see src/plugins/fidelidade/README.md). Run "fayz doctor" to check boundaries.
O que nasce em src/plugins/fidelidade/:
| Arquivo | Papel |
|---|---|
index.ts | O PluginManifest: id, navegação, rotas e a página. O mesmo contrato de um @fayz-ai/plugin-*. |
data/types.ts | O contrato de dados (FidelidadeDataProvider) que todo backend implementa. |
data/mock.ts | Provider mock — o plugin já roda sem banco. |
data/supabase.ts | Provider real, passando pelo boundary do Fayz (getSupabaseClientOptional), nunca importando o SDK do Supabase direto. |
schema/index.ts | Onde as migrations do plugin vivem. |
README.md | Como fiar o plugin e a checklist de graduação. |
O contrato é o mesmo dos oficiais
O index.ts exporta uma factory que devolve um PluginManifest — id, navigation, routes e uma página. Ele escolhe o provider automaticamente (Supabase quando há backend, mock caso contrário) via createSafeDataProvider:
export function createFidelidadePlugin(options?: FidelidadePluginOptions): PluginManifest {
const provider =
options?.dataProvider ??
createSafeDataProvider(
() => createSupabaseFidelidadeProvider(),
() => createMockFidelidadeProvider(),
)
// ...
return {
id: 'fidelidade',
name: 'Fidelidade',
icon: 'Puzzle',
version: '0.1.0',
navigation: [{ section: 'main', position: 50, label: 'Fidelidade', route: '/fidelidade', icon: 'Puzzle' }],
routes: [{ path: '/fidelidade', component: Page }],
}
}
Para ligá-lo, adicione createFidelidadePlugin() ao array de plugins da config do app (o README.md gerado mostra onde). Depois confira os limites:
npx @fayz-ai/cli doctor
O boundary é obrigatório
O data/supabase.ts gerado nunca importa @supabase/supabase-js direto — ele obtém o cliente por getSupabaseClientOptional(), o boundary do Fayz, e escopa a query por tenant_id:
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
// ...
const { data, error } = await sb()
.from('fidelidade')
.select('*')
.eq('tenant_id', getActiveTenantId())
Se você trocasse isso por um import direto do SDK do Supabase, o fayz doctor acusaria a quebra de boundary. Essa é a regra que mantém o seu plugin promovível — o detalhe está em Padrões.
Onde vão as migrations
As tabelas do seu plugin vivem em schema/index.ts e são referenciadas no manifesto via migrations: [...]:
migrations: [{ id: 'fidelidade-0001', version: '0.1.0', sql: '<create table ...>' }]
No fayz db apply, o passo incubator (o último da ordem) aplica essas migrations junto com o resto — por isso a ordem spine → drizzle → seed → plugins → incubator importa.
De incubator a oficial
Promover um plugin app-local para um pacote @fayz-ai/plugin-* oficial é empacotamento, não reescrita: o manifesto não muda, você move a pasta para um pacote próprio. O README.md gerado traz a checklist completa de graduação (contrato validado, teste de capacidade, migrations fiadas, permissões declaradas, i18n completo, boundary limpo). O mesmo contrato que você seguiu aqui é o que os plugins do catálogo seguem — não há nada que um plugin oficial faça que o seu não possa.
Próximo: cada campo que um plugin pode declarar em Manifesto do plugin.