Upgrade Transparency
When you upgrade a customer’s install to a new product release, Fullfinity migrates its own
data and standard modules for you (see Migrations & Upgrades —
the schema ledger carries renames, drops, and retypes and replays them atomically). What an
upgrade cannot do is reach into the modules you wrote and fix the places they reference
something a release moved. If a release renamed SaleOrder.total to grand_total, a custom
view of yours that still names total is now stale.
Fullfinity does not try to prevent that break — you are free to rename fields, and so are we. Instead it makes the break transparent, precisely located, and fixable by you, locally:
preview --db— a read-only pre-flight you run before upgrading. It lists every reference in your custom modules that no longer resolves against the new base.preview— the human-readable list of every reference-affecting change a release shipped, that each report line links back to.
The workflow
Section titled “The workflow”Point the install at the new product code, then, before running the upgrade:
./fullfinity-server -c config.yaml preview --db <db>This changes nothing — it only reads. It exits 0 when your custom modules are clear to
upgrade, or 1 with a punch-list when something needs fixing:
# Upgrade reference report
2 custom-module reference(s) need attention before upgrade:
## acme_sales- acme_sales_order_form — field references field 'total' which no longer exists on SaleOrder → renamed to grand_total (#42)
## acme_hr- acme_payslip_form — field references field 'legacy_code' which no longer exists on Employee → removed (#39)Fix the flagged views/templates in your modules, re-run preview --db until it is clean, then
upgrade normally:
./fullfinity-server -c config.yaml -db <db> -u allBecause you fixed the references first, the upgrade composes your modules cleanly and completes in one atomic transaction.
If you upgrade without the pre-flight
Section titled “If you upgrade without the pre-flight”If you run -u all straight away and a custom module references something the release moved,
the upgrade rolls back atomically — nothing is changed — and tells you to run preview --db
for the full list. You never end up on a half-applied database or with silently-broken screens
in production; you either fix first and upgrade clean, or the upgrade declines and points you at
the report.
What preview --db checks
Section titled “What preview --db checks”Every place you declare a reference to a model or field — anywhere it’s stored as data, not just in views — is audited against the new base:
- View field references — a
field/linkButton/statusbar/browsepanelbound to a model field that no longer exists. - View
visible/readonly/required/filterconditions — aQ(...)expression referencing a renamed or removed field (including across relations). - View inheritance targets — an inheriting view whose
anchor/field target no longer resolves in the base it patches. - Record-rule domains, window/portal action filters — a
Q(...)over a model whose field was renamed or removed. - Automation, approval, campaign filters — a visual filter-group condition on a field that no longer exists.
- Config-gated visibility — a menu
visible_settingreferencing a Configuration setting that was renamed. - Field-path settings — an approver field, a contact field, an order-by, a “field to set” pointing at a field that’s gone.
- Model references — a stored model name (e.g. a record rule’s
model) that no longer exists after a model rename/removal. - Report-template patches — a template extension whose
#id/[data-anchor]target is gone from the base template.
Each finding is cross-linked to the changelog by its change #id, so “field not found” becomes
“renamed to grand_total (#42)”.
The same checks run at save time on the records themselves: creating or importing one of these records with a stale reference raises immediately. That’s what makes the upgrade rollback above work — a custom module whose reference broke can’t re-import silently.
Honest limits
Section titled “Honest limits”preview --db sees what is declared. It cannot see custom Python — a record.total
attribute read or a method call in your module’s code is invisible to a static pass. For those,
read the changelog and grep your module code for the old names. Report-template bodies are
opaque text; Fullfinity scans them best-effort for attribute reads (doc.total) whose name
exactly matches something the release renamed or removed, and marks those advisory (verify
manually). The report states these limits inline.
Writing report templates so they stay validatable
Section titled “Writing report templates so they stay validatable”A report template renders against one bound model (named by the report action). So that a field a report reads can be checked against the schema, every name in a report body resolves to exactly one place:
| You write | It resolves to | Renamed field caught? |
|---|---|---|
a bare {{ total }}, {{ contact.email }}, {% for l in lines %}{{ l.amount }} | a field on the report’s model | yes |
{{ config.inventory_use_lots }} | a Configuration setting | yes |
{{ report.tax_lines }}, {% for c in report.components %} | your report action’s data_method output | no — computed data, not a field |
{{ report_data.x }} | a wizard/model-less report’s input | no — computed data |
{{ company.name }}, {{ paper_format.margin_top }}, {{ t('…') }}, {{ qr(…) }} | render globals | company/paper_format: yes |
The rules that follow from this:
- A bare name is always a field on the report’s model. Don’t read a calculated value as a
bare name — put it behind
report.(return it from the action’sdata_method). - Gate on a setting with
config.<setting>, not adata_methodboolean.config.*is validated against the Configuration model, so renaming a setting is caught in reports too. - A
data_methodreturns only computed extras, read asreport.<key>. Everything the record already has is a bare field.
A report with no bound model (a wizard/aggregate report) reads its data from report_data.*
and has no bare-field checks — there’s no model schema to resolve against, and a model-field
rename can’t break a report that references no model.
Writing email templates so they stay validatable
Section titled “Writing email templates so they stay validatable”An email template is the same kind of surface: it renders against one bound model (the
model named on the template), so its content and subject are both validated against that
model’s schema. The globals differ from a report — an email is not a paginated document, so it
gets no report chrome (config, paper_format, report, report_data). Instead every email
render is handed two universal globals:
| You write | It resolves to | Renamed field caught? |
|---|---|---|
a bare {{ name }}, {{ contact.email }}, {% for l in lines %}{{ l.amount }} | a field on the template’s model | yes |
{{ company.name }} | the sending company | yes (validated against Company) |
{{ base_url }} | the install’s public URL, for links back to the app/portal | n/a — a scalar global |
{{ t('…') }}, `{{ … | date }}` | render callables/filters | n/a |
- Never hand-assemble a Jinja environment to inject extra context — render through the
template’s own render path so
base_url/companycome from the one globals surface. A caller that needs to override a global (e.g. a storefront passing its own domain asbase_url) does so with extra context, not a bespoke renderer, so the body stays validatable by construction. - A template with no bound model (a pure notification) has no bare-field checks — only its
company/base_urlchains are validated.
Both surfaces are enforced the same three ways: at save time (creating/updating a report
action or email template with a stale body raises, so install and -u reject it and roll back),
at preview --db (the install-specific punch-list lists every stale body), and by the
render/body smoke tests that render every report and validate every email body against demo
data.
The changelog
Section titled “The changelog”./fullfinity-server preview # to stdout./fullfinity-server preview --out CHANGELOG.md # to a fileIt renders every reference-affecting ledger change — field and model renames and removals — grouped by release (derived from the repository’s release tags when present; the changes since the newest tag appear under Unreleased), with a separate section for data-only migrations that carry no reference impact. No database is needed.
The static reference gate (check --only refs)
Section titled “The static reference gate (check --only refs)”Every stored reference is validated at three points, tightest-loop first:
- Save time — creating/updating a record with a dangling reference raises, so install and
-ureject it and roll back. This is what makes an upgrade refuse a stale reference. check --only refs— a no-database build-time gate that composes the model surface from code and validates every shipped reference against it. It runs as part ofcheck(CI +.githooks/pre-commit), so a rename that orphans a reference is caught at commit time — before any install.preview --db <db>— the install-specific pre-flight that audits a partner’s custom modules against a new base (see above).
./fullfinity-server -c config.yaml check --only refs # exit 1 on any dangling reference./fullfinity-server -c config.yaml check --only refs --module sales # scope to one moduleThe gate covers the same surfaces the save hooks do — Q-domains, JSON filter-groups,
visible_setting, field-paths, model-names, and report/email Jinja bodies — resolving each
reference’s model the way the record does (a model: in YAML is a plain class name). It needs no
database and no install: it composes the field surface straight from the module source.
For CI pipelines
Section titled “For CI pipelines”The build-time gates are no-database and deterministic. One verb — check — runs all of them
(schema, views, references, Valkey-access, manifests) and reports every failure together, so CI
and pre-commit block a merge that breaks the schema, a view’s stability, a stored reference,
Valkey isolation, or a module manifest:
./fullfinity-server -c config.yaml check || exit 1 # all gates; --only <area> for onepreview --db exits non-zero when it finds anything, so a partner pipeline can additionally gate an
automated upgrade on a clean pre-flight against the target install:
./fullfinity-server -c config.yaml preview --db <db> || exit 1 # block the upgrade until clean