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 and Theming. Recomposing (level 3) means assembling a page as a block tree — pure data:
{
"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:
// 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):
// 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)
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.
{
"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.
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.
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.
Next: the second consumption path in Headless.