# Patterns

A plugin that follows the contract *works*. A plugin that follows the **patterns** composes: it coexists with the other plugins, accepts a real backend without a rewrite, and can be promoted to official. This page is the set of conventions that make that difference.

## Factory, not instance

A plugin is exported as a **factory** — a function that returns the `PluginManifest` — not as a ready-made object. That lets whoever assembles the app inject options (a provider, a menu position, a vertical) without editing the plugin:

```ts
export function createLoyaltyPlugin(options?: LoyaltyPluginOptions): PluginManifest {
  // ...
}
```

Wiring the plugin up means calling the factory in the plugins array. Configuring it means passing options. It's never editing the plugin's internals.

## Provider-first: data is a contract

A plugin's UI never talks to a database directly. It talks to a **`DataProvider`** — an interface every backend implements. The plugin defines the contract in `data/types.ts`, ships a `mock` and a `supabase` one, and picks between them with `createSafeDataProvider`:

```ts
const provider =
  options?.dataProvider ??
  createSafeDataProvider(
    () => createSupabaseLoyaltyProvider(), // used when a backend is available
    () => createMockLoyaltyProvider(),     // fallback without a database
  )
```

The payoff: the plugin runs on day zero (mock), and switching to real data touches no screen — it only switches the provider. It's the same pattern that makes a plugin testable without infrastructure (see [Test and debug](/en/docs/test-and-debug)).

## The provider boundary: talk to Fayz, not to the provider

The rule `fayz doctor` polices: an app or plugin does **not** import provider SDKs directly (`@supabase/supabase-js`, `mercadopago`, `stripe`, `googleapis`, …). It gets access through the Fayz boundary — `getSupabaseClientOptional()` / the `DataProvider` interface — or through the connector spine.

```ts
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'
// ✔ goes through the Fayz boundary, scoped by tenant

// import { createClient } from '@supabase/supabase-js'
// -> doctor would flag this as a boundary break
```

There's one deliberate exception: an app-local plugin **may** own a connector + Edge Function for a provider Fayz doesn't support yet — because the credential stays on the server and the app still calls its *own* boundary, not the provider from the browser. That exception is declared with a marker on the import line, not casually.

## The supported surface

An app only depends on packages from the supported public surface — `@fayz-ai/sdk`, `core`, `saas`, `ui`, `auth`, `db`, `storefront`, `shop` and the `@fayz-ai/plugin-*` ones. Importing an internal subpath or a package outside that list makes doctor warn (`off-surface-import` / `off-surface-dependency`). The rule exists to keep upgrades safe: anything not on the surface can change without notice.

## `plg_` tables and core reuse

By convention, a plugin installs **its own tables** with the **`plg_`** prefix (which is what shop and courses do — `plg_shop_products`, `plg_courses_*`) to make ownership obvious. The convention is **recommended, not universal**: not every module follows it — the openbanking connector, for instance, creates `bank_integrations` with no prefix. What the plugin does **not** create, it **reuses**: it references the [core](/en/docs/data/model) library tables it needs instead of copying them. A plugin never redefines a core entity; if it needs to know "who booked", it references `public.people`. One source of truth per entity.

The migrations a plugin ships follow rules that make `fayz db apply` reproducible:

- **ordered** — the apply sequence is deterministic.
- **versioned** — every migration has a version; the runner knows what already ran.
- **fix-forward** — migrations are append-only: never edit one that's already been applied, write the next one that fixes it. There is no `.down.sql`.
- **idempotent** — running again neither breaks nor duplicates.

## Isolation, always

Every `plg_` table a plugin owns carries `tenant_id` and RLS in the canonical form. This isn't optional or a "later" thing — it's the condition for the table to exist in the model at all. The details are in [RLS and multi-tenancy](/en/docs/data/rls).

{% callout type="info" %}
Enforcement is **soft** by design: `fayz doctor` reports these violations as *warnings*, it doesn't fail the build. The idea is visibility without getting in the way of the DX while you're experimenting. But a plugin that wants to graduate to official needs a clean doctor run.
{% /callout %}

---

Next: exercise the app and catch breakage early in [Test and debug](/en/docs/test-and-debug).
