The external Solution lifecycle is planned, not product-complete in Core. The
boundaries below are enforced today; the installation and release tooling around
them is still being built. Treat this page as the contract you will be held to,
not as a finished walkthrough.
Boundaries
A Solution must not live underapp/Solutions inside Core. It is an external
package, resolved through Composer like any other dependency — see
Development setup for the two-lane workflow.
Its public contracts belong in fapost/foundation. Primitives with no Core
dependency belong in fapost/support. Neither package may reference App\…, and neither may
absorb business logic that belongs to Core.
Front-end components are possible, but only through the agreed build and publish
contract. A Solution cannot drop Vue files into a running application — the
builder front end is compiled ahead of time.
What a Solution typically contains
Most Solutions are a combination of pieces documented elsewhere in this section:Node handlers
So flows can act on your domain.
A data accessor
So conditions can read your data without duplicating it.
Builder configuration
So your nodes can be configured on the canvas.
Migrations
Subject to the same migration isolation rules as Core.
Limiting what a tenant can create
If your Solution has records an operator should be able to cap per tenant (for example candidates), register a limit key from your service provider’sboot(). Core binds the registry in its own
provider, which can run after a package’s register(), and closes the registry once the application
has booted, so do not register from register() or from a booted() callback:
snake_case and stable, because plans store limits under it; the label and unit are in
English. An operator package reads the registry to offer the limit in its plan form, and answers how
much each tenant may have through TenantLimitsInterface. Registering the key is the Solution’s
whole contribution to the contract: it does not decide the number.
Registering the key makes the limit exist; enforcing it is your Solution’s job. Create the records
through one service that counts the tenant’s existing ones and asks RecordQuotaInterface before
saving, and send every creation path through that service:
RecordLimitReachedException with a message fit for the person who sees it; catch it
where you show errors. It works in the tenant context of the current request or job. Records that
already exist stay when a limit drops: only creating more is refused. See
Foundation contracts for the interfaces.
Limiting how much a tenant does in a period
For work that is counted over time rather than stored, such as messages sent or calls made, register the key fromboot() the same way with kind: LimitKind::PerPeriod. Nothing is stored in a tenant
schema; the operator package counts. Spend one unit before the work, with a natural key that is the
same on every retry, and do the work only when the answer is allowed:
Throwable around it, report it and let the work through, which is what
Core does. Build the UsageUnit before anything irreversible, and never put a personal identifier in
the unit key. See Foundation contracts
for what the implementation promises.