Routing and navigation
In a Fayz app you don't write a router. Navigation — the menu items and the screens behind them — is derived from what the manifest declares: the plugins wired into the surface, plus any extra pages you define. This page shows where each route comes from.
The surface is the root
Every route is born inside a surface. A surface is one face of the app (admin, storefront, member); it declares the scaffold (the shell layout), the list of plugins, and an optional list of pages:
{
"surfaces": {
"admin": {
"scaffold": "admin",
"plugins": [{ "id": "dashboard" }, { "id": "crm" }],
"pages": []
}
}
}
From the block above, the runtime assembles the entire navigation. You wire up a plugin and its menu and screens show up — without touching a single route file.
Where a plugin's route comes from
Each plugin describes its own navigation and its own routes in its manifest (the PluginManifest). Two fields:
navigation— the menu items. Each entry has asection(main,secondary, orsettings), aposition(ordering), alabel, aroute, and an optionalicon. This is what paints the sidebar.routes— thepath → componentmapping. Each route points to a component (directly or via a registrycomponentId) and can declare aguard(authenticated,public,role,share-token).
When you add { "id": "crm" } to the surface, the runtime reads the CRM's navigation and routes and injects both: the menu item and the screen at /crm. Wiring up two plugins merges both navigations into the same sidebar, ordered by section + position. That's why the order of the ids in the manifest doesn't determine the visual order — each plugin's position does.
Your own pages, no plugin required
When you need a standalone screen that doesn't belong to any plugin, declare it in pages. Each page is pure data and points to exactly one content source:
{
"pages": [
{ "path": "/about", "label": "About", "section": "secondary",
"blocks": [{ "type": "hero", "props": { "title": "Our story" } }] },
{ "path": "/report", "label": "Report", "component": "custom:harvest-board" }
]
}
The schema requires one of three keys on the page:
blocks— a block tree resolved by the block registry (recompose content without code).entity— an entity id, to generate a CRUD over it.component— the id of your own component registered insrc/registry.tsx(thecustom:namespace).
The label, icon, section, and permission fields control how the page shows up in the menu and who can see it.
The registry connects ids to code
Routes and pages reference code by id, never by inline import. A component: "custom:harvest-board" in the manifest only resolves because you registered that id in src/registry.tsx:
// src/registry.tsx
import { registerPage } from '@fayz-ai/core'
import { HarvestBoard } from './pages/HarvestBoard'
registerPage('custom:harvest-board', HarvestBoard)
registerPage (and its siblings registerComponent / registerBlock) come from @fayz-ai/core — that import compiles today. The src/lib/fayz-runtime.ts generated by the scaffold is still an anchor screen and doesn't re-export these functions yet; it will soon. Until then, import directly from @fayz-ai/core.
That separation — data in the manifest, code in the registry — is what keeps the app upgradable: the platform knows exactly which ids you overrode, and fayz doctor can report divergences.
Rule of thumb: a capability = wire up a plugin (the menu grows on its own); a standalone screen = an entry in pages; your own code = a custom: id in registry.tsx, referenced as data. You never edit a router.
Next: understand the whole file in Configuration.