# Customization

The promise is "nothing locked in": a Fayz app goes from pure config to fully bespoke **without ever forking the SDK**. Customization is a seven-level ramp, each level strictly additive — someone sitting at level 6 keeps getting SDK upgrades for everything they didn't touch.

## The ladder

| Level | What changes | Where | Code? |
| --- | --- | --- | --- |
| 1 · Config | labels, currency, flags, plugin config | `app.manifest.json` | no |
| 2 · Theme | brand + tokens | `theme` in the manifest | no |
| 3 · Recompose | reorder/add/remove blocks, new pages | `surfaces.*.pages[].blocks` | no |
| 4 · Slots | inject widgets into named zones | widget refs in the manifest | sometimes |
| 5 · Override | replace an SDK component/block by id | `src/registry.tsx` | yes |
| 6 · Custom pages/blocks | bespoke React, routed by the manifest | `src/registry.tsx` + components | yes |
| 7 · Your own plugin | your own entities/migrations/nav/blocks | a plugin package | yes |

Levels 1–4 are **platform-editable** — the Fayz UI / AI agent edits the manifest live, no code and no deploy. Levels 5–7 are code in your repository, but **confined to `src/registry.tsx` + your own components** — never copies of SDK pages.

## Levels 1–4: data only

You've already seen config and theming in [Configuration](/en/docs/apps/configuration) and [Theming](/en/docs/apps/theming). **Recomposing** (level 3) means assembling a page as a block tree — pure data:

```json
{
  "pages": [
    { "path": "/", "blocks": [
      { "type": "hero", "props": { "variant": "banner" } },
      { "type": "products", "props": { "title": "Highlights", "limit": 4 } }
    ]}
  ]
}
```

**Slots** (level 4) inject widgets into named zones of the shell (`shell.topbar.end`, `shell.sidebar.footer`, `page.after`, `shell.floating`, …). You reference a registered widget; for a widget of your own, register it (level 5/6) and cite it by id.

## Level 5: override by id

Every SDK component or block has a **registry id**. Re-register that id in `src/registry.tsx` and your version wins (last-registration-wins). It receives the **same typed props** as the original — so the SDK can evolve the internals behind the props contract without breaking you:

```tsx
// src/registry.tsx
import { registerComponent, registerBlock } from '@fayz-ai/core'
import { BrandedDetailHeader } from './components/BrandedDetailHeader'
import { FancyHero } from './components/FancyHero'

registerComponent('crud.detail-header', BrandedDetailHeader) // swaps out an SDK component
registerBlock('hero', FancyHero)                             // swaps out the built-in hero block
```

## Level 6: `custom:` pages and blocks

React that's entirely yours, in the `custom:` namespace (the platform never generates `custom:` ids — they only come from your repository). Register it and reference it by id in the manifest. Your component runs with the full SDK context (data provider, tenant, permissions, i18n):

```tsx
// src/registry.tsx
import { registerBlock, registerPage } from '@fayz-ai/core'
import { WineStory } from './blocks/WineStory'
import { HarvestBoard } from './pages/HarvestBoard'

registerBlock('custom:wine-story', WineStory)
registerPage('custom:harvest-board', HarvestBoard)
```

{% callout type="info" %}
The `register*` functions (`registerComponent`, `registerBlock`, `registerPage`) come from `@fayz-ai/core` — the import above compiles today. The `src/lib/fayz-runtime.ts` the scaffold generates is, for now, an **anchor screen** (just a `renderApp` placeholder) and does **not** re-export these functions yet; the scaffold will re-export them soon. Until then, import directly from `@fayz-ai/core`.
{% /callout %}

```json
{
  "pages": [
    { "path": "/harvest", "component": "custom:harvest-board" },
    { "path": "/", "blocks": [{ "type": "custom:wine-story", "props": { "year": 2019 } }] }
  ]
}
```

## Level 7: your own plugin

When the customization is a reusable capability (entities, migrations, navigation, blocks), the ceiling is a plugin — the **same `PluginManifest` contract** the official plugins use. There's nothing an SDK plugin can do that yours can't. That's the subject of [Building plugins](/en/docs/building-plugins/incubator).

## Why upgrades stay safe

- Overrides are keyed by id and receive the original's typed props → SDK internals change without breaking you.
- Your own code stays confined to `src/registry.tsx` + your components + your plugins; you never copy an SDK page, so an upgrade touches everything you did **not** customize and leaves your overrides intact.

{% callout type="tip" %}
There's no "eject" by design. If you feel like you need to fork an SDK page, that's an SDK gap worth reporting — not a supported flow.
{% /callout %}

---

Next: the second consumption path in [Headless](/en/docs/apps/headless).
