dist/ — and the browser loads them by name, running on
the console’s own Vue, Inertia and kit. Nothing is compiled into Core, and
installing a Solution needs no Node and no rebuild of Core.
The console described here is the new Inertia console, which is switched on with
UI_INERTIA while it replaces the Filament panels. With it off, nothing on this
page is registered.
How it fits together
- Your provider’s
boot()declares an application, actions, widgets and so on throughConsoleExtensionsInterface. Core ties every declaration to your Solution’sidand closes the registry when the application has booted. platform:updatechecks yourfapost-ui.jsonagainst those declarations and copies your built files topublic/solutions/<id>/<hash>/.- On every console request Core keeps only the declarations the request may see:
your Solution is active for the tenant and its UI is published, the user has the
declaration’s
ability, and — for what lives on an assistant — an assistant is open. Labels are translated on the server. The rest never reaches the browser. - The browser renders those points and, when one is shown, loads its component
from your bundle. Pages are loaded the same way when one of your routes renders
<id>::<Path>.
Registering
Requirefapost/foundation ^0.15, override boot() in your provider, call
parent::boot() first, then declare:
boot() and nowhere else. A call from register(), from a
booted() callback or from a request throws InvalidConsoleDeclarationException.
Do not use bootSolution() or registerExtensions() for this: Core does not
implement CoreRegistrarInterface yet, so AbstractSolutionServiceProvider::boot()
returns before it calls them.
The registry also throws when a name is outside your namespace, when you declare
a second application, and when a key repeats within the same point and target.
The DTOs check formats when they are constructed and throw
InvalidArgumentException:
The six points
The targets are enums: a target Core does not list cannot be written. New targets
arrive as new cases in a Foundation minor.
Application. At most one per Solution, keyed by your Solution
id, so a
Solution whose id is console or admin cannot have one. entryRoute is where
the rail opens it; menu lists MenuItems (label, route, icon, ability, an
optional group heading) in display order. With perAssistant: true it lives on an
assistant like the Console does, and is offered only where an assistant is open.
Core’s own menus never receive your entries. Your application is not pinned to the
rail by default; shortLabel defaults to label.
Context action. ContextAction::link() navigates to one of your routes, and
that route takes a record parameter: Core fills it with the id of the contact or
conversation the action was chosen on. A link to a per-assistant route also gets
the open assistant as {tenant}, and is left out where no assistant is open.
Laravel passes route parameters by position, so the action of
assistant/{tenant}/apps/<id>/contacts/{record} takes string $tenant, string $record.
ContextAction::dialog() opens your component in the kit’s dialog instead.
Shell element. Loaded only after the page has rendered, so it never slows a
screen down. With perAssistant: true it is shown only where an assistant is open.
Dashboard widget. WidgetSize::Single, Double or Full — one column, two
columns or the whole row of the dashboard’s grid (two columns on the assistant
console’s dashboard, three on the admin one). A widget fetches its own data from your routes; it gets
nothing from Core’s dashboard request.
Settings tab. A form of its own, with its own routes and storage; Core’s save
button does not submit it. The tab is addressed as ?tab=<id>.<key>. On both
settings screens a Solution’s tabs join Core’s tab bar after Core’s own tabs; while
one is open, Core’s form and its save button are hidden.
Table column. Core renders the cells; you supply only the values, through a
class implementing ColumnValuesInterface:
$context->key; $context->assistantId is the open
assistant, if any. Return scalars or null keyed by row id; a missing id renders
an empty cell.
The cell type is CellType::Text, Badge, Number (formatted in the user’s
locale), Date (shown relative, the exact value on hover) or Status with
tones mapping a value to Tone::Neutral, Success, Warning, Danger or
Info. A column cannot be sorted or searched, and a cell cannot be your own
component, in this version.
Routes and pages
Register routes through the contract, never on your own. Core adds what you may not choose yourself: the console’s domain, the prefix, the names and the middleware.
Both groups are the console’s own stack — session, tenant, signed-in active staff
user — followed by a check that answers 404 while your Solution is not active
for the tenant or its UI is not published.
solution.assistant also resolves the
assistant from {tenant}, as the Console’s routes do. When your application
declares an ability, every route also requires it.
The name you give a route is prefixed with solutions.<id>.: ->name('board')
becomes solutions.showcase.board, which is the name your declarations use. Use
controller actions, not closures, so the routes survive route:cache. Authorize
every action with your own policies: the menu entry a user cannot see is still a
URL they can type.
A page is an Inertia response with a page of your own:
layout, and marks your application as the current one in the
rail.
Names
Pages and components are named<id>::<Path>: your Solution id, then
PascalCase segments joined by /, such as showcase::Board/Index or
showcase::Widgets/Stats. Core’s own pages have no namespace. Pages and
components share one namespace, so a Path names exactly one file. Since names
appear in URLs, asset paths and pins, the rule that a Solution’s id never changes
protects them too.
Building the bundle
Build your front end into a directory of its own, such asdist/, with
fapost-ui.json next to the files. Every page and component is an ES module whose
default export is a Vue component. Keep these specifiers external — do not
bundle them:
vue@inertiajs/vue3@fapost/uiand its public subpaths:@fapost/ui/components/alert-dialog,avatar,badge,button,card,checkbox,command,confirm-dialog,data-table,dialog,dropdown-menu,empty-state,form-field,form-section,input,label,select,separator,sheet,skeleton,sonner,switch,table,tabs,textarea,tooltip, and@fapost/ui/lib/utils,@fapost/ui/lib/relative-time
fapost-ui.json
Files are relative to the directory of
fapost-ui.json, made of letters, digits,
., _ and -, with no segment starting with .. Pages and components together
list at least one entry. Any other field is an error. To check the file without
Core, run Fapost\Foundation\Console\Bundle\UiBundleParser, which reports every
violation at once.
Point the manifest at it with extra.fapost.ui, a path relative to the package
root that ends in .json:
dist/ in the package, or build it before the package is
installed: the image that runs Core has no Node.
Publication
php artisan platform:update checks the bundle of every installed Solution with
extra.fapost.ui together with the manifests, before any migration, and
--validate-only runs the same check. Each of these fails the command and changes
nothing:
fapost-ui.jsonis missing, is not valid JSON, or breaks the schema;- a listed page, component or stylesheet does not exist inside its directory;
- a component named in your PHP declarations is not listed under
components; fapost-ui.jsonsits at the package root rather than in a directory of its own;- a file in that directory has PHP anywhere in its name (
x.php,x.php.js,x.phtml), since a web server may execute it; - a link leads outside that directory.
public/solutions/<id>/<hash>/, where <hash> comes from the content of
fapost-ui.json — your build’s hashed file names make every build a new
directory, served as immutable. Only fapost-ui.json and files with these
extensions are copied:
js, mjs, css, map, woff, woff2, ttf, otf, png, jpg, jpeg,
gif, webp, avif, ico
No SVG (it can carry script), no HTML. Any other file is left behind with a
warning, since a module that imports it would fail in the browser; a listed page,
component or stylesheet that could not be copied is an error. Hidden paths, such
as .git/ or .env, are skipped without a word. The current build and the one
before it are kept, so a browser still holding the old page map can finish
loading; older builds are removed.
A Solution whose build changed since the last platform:update is not served
until the command runs again. The console’s asset version includes the published
bundles of the tenant’s active Solutions, so activating, deactivating or updating
one reloads the page on the next visit.
Trust and failures
Same origin means full trust. Your JavaScript runs with the signed-in user’s session: it can call any console endpoint as that user and read the screen it is mounted on. That adds no new trust — your PHP already runs with the application’s full rights, and installing a Solution is the decision of whoever builds the image. So authorization stays on the server, secrets stay out of props, and a component mounted on Core’s screens receives nothing butrecordId and assistantId (where
they apply). This holds only for code an operator installs; a Plugin ships no Vue.
A failure stays yours. A component on Core’s screens whose name is not in the
map, whose module does not load within ten seconds, or which throws while
rendering is hidden, with the error in the browser console; the screen keeps
working. A page that cannot load becomes a “Solution unavailable” page inside the
shell rather than a blank screen. A column whose values() throws is left empty
and the error is logged. A link action whose route does not exist is left out.
The boundary
Not ready yet
- The Vite preset that builds
dist/and writesfapost-ui.json, and the published@fapost/uipackage to build against, are still to come. Until then a bundle is built by hand or with your own Vite configuration in library mode, with the specifiers above external. - The version check.
kit,vueandinertiaare required and recorded but not yet compared with the console’s: a bundle built against an incompatible kit is served anyway. - The derived image. Publication writes to the
public/of the container that runsplatform:update. The Docker Compose installation serves static files from its separatewebimage, which does not see that directory, so it does not serve/solutions/yet; the recipe for an image with a Solution’s assets published is still to be written. A single-tree install — development, bare metal — serves them as soon as the command has run. - The activation screen. Activation is switched with
solutions:activateandsolutions:deactivate; see Building a Solution.