Enterprise Licensing
Some of Fullfinity is licensed. A module you write runs on installs that hold an Enterprise licence and installs that do not, so it needs to know what changes between the two — and, more usefully, what a custom module simply cannot reach without one.
Licensing is enforced server-side, in the engine, on two independent questions:
| The question | What it protects | What a violation looks like |
|---|---|---|
| Whose data is this? | Records, fields and inherited-view contributions belonging to a licensed module | 403 with code: ENTERPRISE_LICENSE_REQUIRED |
| Whose capability is this? | Licensed view types and client actions — the enterprise screens themselves | The capability is replaced by a locked placeholder |
Neither is a client-side check. Nothing you can declare in YAML, and nothing the browser can be persuaded to send, changes either answer.
The data half
Section titled “The data half”A licensed module owns its models, and it owns the fields it adds to other modules’ models
through __inherit__. On an install without an active licence:
- Reading or writing a record of a licensed model raises an
AccessErrorsubclass carryingcode: ENTERPRISE_LICENSE_REQUIRED(see Model Access for how that reaches your code, and why thecodeis withheld from public surfaces). - Serialization omits the value of a field owned by a licensed module. The column and the data are untouched — nothing is deleted, and activating a licence brings the values straight back — but an unlicensed request does not receive them.
- Menus and inherited-view sections contributed by a licensed module are excluded when views are composed, so they are absent from the screen rather than present and broken.
The consequence for your module: do not assume a field added by a licensed module is present in a payload. If your code needs to behave differently when one is missing, treat “absent” as the unlicensed case rather than as an error.
The capability half
Section titled “The capability half”A capability is a name the frontend dispatches a renderer on — a UiView’s type or a
WindowAction’s client_action. Some of those names belong to Enterprise:
Licensed view types (UiView.type)
Gantt · TimeGrid · TimelineHeatmap · FieldDeck · DocumentBrowser
Licensed client actions (WindowAction.client_action)
FinancialReport · BankReconciliation · CustomerStatements · PaymentMatching ·
PlanningGrid · ShiftScheduler · Shopfloor · Scanner · EcoDiff
Declaring one of these from a module of your own does not give you the component. The gate
keys on the name, not on which module declared it and not on which model it points at — so
a custom module that declares type: Gantt against its own free model is refused exactly as a
licensed module is on a lapsed licence. It is refused at both points a capability becomes
reachable: when a view’s arch is composed, and when an action is built.
Everything else in the view vocabulary is free and unaffected — Form, List, Kanban,
Search, Wizard, Pivot, Chart, Calendar, Map, Dashboard, OrgChart,
PortalList, PortalDetail, and the free client actions.
What an unlicensed client receives
Section titled “What an unlicensed client receives”Rather than an error or an empty screen, the server substitutes a locked placeholder and the app renders an upgrade prompt in its place. Concretely:
- A client action that is licensed comes back as
client_action: "Locked", withlocked_capabilitynaming what was withheld. No arch, no fields, no model binding — the enterprise component is never named, so it cannot mount and its data method never runs. - A licensed view type is dropped from the action’s views, leaving the free views beside
it working. An action with
modes: List,Ganttis still a usable List on an unlicensed install; only if that leaves the action with nothing at all does it become the locked placeholder.
So an action that mixes free and licensed views degrades rather than disappears. That is worth designing for: put the licensed view alongside a free one and unlicensed users keep a working screen.
The grace period, and what follows it
Section titled “The grace period, and what follows it”An expired licence has a 14-day grace window before anything is withheld. Both halves fold it in from the same place, so an instance that is lapsed-but-in-grace behaves exactly as a licensed one — its views compose, its actions build, its data serializes. Do not write your own expiry arithmetic against a licence date; there is no case where you need to, and re-deriving it turns a soft warning into an outage on the first day of the window.
Once the window closes the install does not enter a reduced mode — it stops. Staff cannot use the application until someone renews or downgrades to Free, and both of those remain available throughout. There is no third state in which enterprise modules are installed, unlicensed, and part of the product still works.
Two consequences are worth designing around:
- A blocked install blocks your module too, free or not. Its backoffice endpoints and its scheduled work do not run, because the stop is a fact about the installation rather than about any one module. Nothing you write changes that, and nothing you write should try to.
- The customer’s own customers keep working. Portal contacts, storefront shoppers and anonymous visitors are unaffected: their requests are served, and licensed values on the pages they load are not withheld from them. A public page that happens to touch a licensed value renders whole rather than failing partway through. So a portal or storefront feature you ship behaves the same on a lapsed installation as on a licensed one.
Building a module that spans both editions
Section titled “Building a module that spans both editions”- Depend on a licensed module and yours becomes licensed in practice — its data is gated,
so your feature stops working when the licence lapses. If you want your module to work on
both editions, do not put a licensed module in
depends. - A licensed view type belongs in a licensed module. If you ship one from a free module it is refused on every unlicensed install, which is a feature that silently does not exist for most of your users.
- Check for absence, not for a licence. Ask whether the field or the record you need is there, and degrade if it is not. A licence test written in your own module is source a customer can edit, so it protects nothing — and it can only drift out of step with the real gate, which already ran.
- A licence test in your own module fails the build.
./fullfinity-server check --only license-apiruns in CI, in pre-commit, and at app-store intake (--module <id>), and rejects any module that reaches for the licence machinery. It is not a style rule: your module ships as source, so a check written in it is one the reader can delete — it protects nothing while reading as though it does. Licensing is decided for you, in one place, for every module. Ask whether the field or record you need is present and degrade if it is not. - Do not build a public feature that expects to be refused. Routes serving anonymous visitors or portal users are not gated, so they behave identically on a lapsed installation.