NocoBase ships fast. Between September 5th and 11th it published six patches. The changelog tells you what the team changed. It does not tell you what will happen to your database, your screens, your workflows.
Someone raised the same concern on the official forum, in Chinese (t/14099). In my translation, the gist is:
I'm on V2.1.30 and don't dare move to V2.2.X, because I don't know what the upgrade will change. The changelog describes it, but the actual impact is hard to judge.
The post also asks for an upgrade matrix: how big each change is, what it affects, and whether you can skip versions.
Two people replied. One upgrades every version in order and never skips one, "for fear of problems." The other says their system is already live and they worry an upgrade will bring bugs. There is no staff reply.
So I counted.
Test setup: NocoBase 2.1.39 → 2.2.14 (official Docker images) + PostgreSQL 16, September 20th. Three instances:
- Upgraded: 2.1.39 installed fresh, a page built through the admin UI, then upgraded in place to 2.2.14 and diffed against snapshots.
- Workflows: measured the same way on a separate instance (2.1.39 fresh, workflows created, upgraded to 2.2.14).
- Fresh: 2.2.14 installed fresh, to compare against the upgraded one.
Table and column counts exclude the collections I created for the test. The screenshots come from a later run of the same steps. Source links point at the
v2.2.14tag.
The short version
| What I looked at | 2.1.39 → 2.2.14 |
|---|---|
| Tables | 100 → 104 |
| Columns | 855 → 899 |
| Dropped tables or columns | none |
| Columns added to existing tables | two |
| A page I built in the UI (one table block) | renders the same |
| Plugin states | 74 enabled, all kept; 2 added |
| Workflow configs | byte-identical |
And one thing that is not in the changelog: the page you build in the admin UI is not stored where you would look for it. More on that below.
The structure barely moves
Four tables appear:
| Table | What it is for |
|---|---|
uiLayouts |
UI layout (new in 2.2) |
desktopRoutesUiLayouts |
join table between the above and desktop routes |
aiUsageEvents |
AI usage records |
userWorkflowTaskStats |
per-user workflow task counts |
Of the 44 new columns, 42 belong to those four tables. Existing tables gain two columns total:
workflows.invalid boolean
aiConversations.scope character varying
Nothing is dropped. Not a table, not a column.
The page still renders
Counting columns is the easy part. The forum's real worry is "will my screens break," so I built one before upgrading: a collection called items with three rows, a page added to the menu through the admin UI, a table block bound to items, and the title column turned on.
Writing the schema straight through the API would have been faster, and it would not have been a valid test. Real users build pages in the UI, and — as it turns out — the UI stores them somewhere else entirely.
On 2.1.39 the page listed three rows. I snapshotted, upgraded to 2.2.14, reloaded the browser, and opened it again. Same three rows, same column order, same "Total 3 items" in the pager. The only difference on screen was the version number in the help menu.
Before the upgrade (2.1.39)
After the upgrade (2.2.14)
The stored definition matched too:
| before | after | |
|---|---|---|
flowModels (the page and its blocks) |
8 rows | 8 rows, every hash identical |
desktopRoutes (the menu) |
2 rows | no diff |
uiSchemas |
67 rows | no diff |
uiSchemaTreePath |
343 rows | no diff |
Where your pages actually live
Here is the part worth keeping even if you never upgrade.
A page created through the admin UI does not go into uiSchemas. It goes into flowModels, and its route has a different type:
| Built in the admin UI | Written to uiSchemas via API |
|
|---|---|---|
desktopRoutes.type |
flowPage |
page |
| Definition stored in | flowModels |
uiSchemas |
| Renders | yes | no |
That last row is not a typo. Insert a Page → Grid → Grid.Row → Grid.Col → CardItem hierarchy into uiSchemas, point a type: "page" route at it, and you get a working menu entry with a blank body. On 2.1.39 and on 2.2.14 alike. uiSchemas is the older engine; the UI does not write to it.
flowModels has three columns — uid, name, and a JSON options — and the whole tree lives in the JSON:
key in options
|
meaning |
|---|---|
use |
model name (TableBlockModel, TableColumnModel, …) |
parentId |
parent's uid
|
subKey / subType
|
which slot of the parent, and whether it is an object or an array
|
sortIndex |
order among siblings |
stepParams |
the model's actual settings |
My one-block page is eight rows:
RouteModel → RootPageModel
RouteModel → BlockGridModel → TableBlockModel(items)
├ TableActionsColumnModel
└ TableColumnModel(title) → DisplayTextFieldModel
The binding to a collection sits in stepParams:
{
"use": "TableBlockModel",
"subKey": "items",
"parentId": "53434fe0b8c",
"stepParams": {
"resourceSettings": {
"init": { "dataSourceKey": "main", "collectionName": "items" }
}
}
}
If you want to verify any of this on your own instance, query flowModels, not uiSchemas:
select uid, md5(options::text) from "flowModels" order by uid;
Run it before and after. Row counts alone will not tell you whether a definition was rewritten.
uiLayouts does not arrive empty
uiLayouts and desktopRoutesUiLayouts are new in 2.2, and it would be reasonable to assume they sit empty until you use the feature. They do not.
uiLayouts : admin-layout-model (desktop) / mobile-layout-model (mobile)
desktopRoutesUiLayouts : both existing routes → admin-layout-model
Migrations in the ui-layout plugin do this. ensure-default-admin-layout creates the two layouts. backfill-admin-layout-desktop-routes then goes through every row in desktopRoutes and attaches each route that has no layout yet to admin-layout-model. A third one, backfill-late-admin-layout-desktop-routes, runs the same backfill again later in startup, for routes that older menu migrations create at that stage.
Nothing in your definitions is rewritten, but these are not empty tables that just show up. A migration reads your rows and writes new ones that reference them.
Plugins and workflows
Plugins: 74 on 2.1.39, all enabled. 76 after, all enabled. The two additions are block-comment and ui-layout — the latter owns the tables above. Nothing was disabled and nothing was removed.
For workflows I built a collection event and an action event (the kind you bind to a button), each with a condition node, then upgraded:
| before | after | |
|---|---|---|
workflows (all columns, config included) |
2 rows | no diff |
flow_nodes (node config included) |
2 rows | no diff |
The configs are small — {"mode": 1, "collection": "orders"} and {"collection": "orders"} — and they came through unchanged. Worth knowing, because a bound workflow silently losing its target is the kind of breakage you find weeks later.
Upgraded and freshly installed end up identical
The other half of the anxiety: does an instance you have been upgrading for a year drift away from a clean install?
I put a fresh 2.2.14 next to the upgraded one and diffed every row of information_schema.columns — table name, column name, type.
| tables | columns | migrations |
|
|---|---|---|---|
| Upgraded | 104 | 899 | 22 |
| Fresh install | 104 | 899 | 18 |
| Diff | 0 | 0 | 4 |
Structurally they are indistinguishable. The path taken is not.
The four extra migrations all belong to the AI employee plugin (plugin-ai/src/server/migrations):
20260407170416-ai-employee-knowledge-base-add-key
20260424000000-remove-cole-ai-employee
20260428175558-update-ai-employee-category
20260429175132-ai-employee-deprecated-orin
All four change existing rows in aiEmployees: three update them (copying knowledge-base IDs into a new knowledgeBaseKeys field, changing three employees' category, marking one as deprecated) and one deletes an employee. Each declares appVersion = '<2.2.0', so it runs only when the instance comes from a version before 2.2. A fresh 2.2.14 install creates those rows in their final form and skips all four. Checked the other direction too: no migration exists only on the fresh install.
So "the schema matches" is true and incomplete. Existing data went through transformations that new data never sees.
One trap while counting
A fresh install reports zero migrations at first.
select count(*) from migrations; -- 0
104 tables, 899 columns, one user, 76 plugins enabled — the install is finished. The table is just empty. Restart the app and it reads 18. Count before that restart and you will conclude that a fresh install runs no migrations at all, which is wrong.
What I did not measure
- Skipping versions. I went 2.1.39 → 2.2.14 in one jump. Whether one-patch-at-a-time behaves differently — the thing the forum thread is actually asking for — is untested.
- Anything more complex than one table block. No forms, no relations, no conditional visibility.
- MySQL. PostgreSQL only.
- Volume. My data was tiny, so migration duration and failure rates are unmeasured.
- Third-party plugins. Stock image.
Downgrading, separately, is not supported. In an earlier, separate test (2.2.5 → 2.1.44), the rolled-back version started but was internally broken.
So
For 2.1.39 → 2.2.14 on PostgreSQL: nothing was dropped, the page I built still renders, plugin states and workflow configs came through untouched, and the result is structurally identical to a clean install.
That is a narrow claim, and getting it meant setting up separate instances and diffing their databases. The request in that forum thread — publish what each upgrade path does — is a fair one.
If you go measure your own instance, remember which table to open. It is flowModels.
(Measured on NocoBase 2.1.39 → 2.2.14 with PostgreSQL 16, September 20th 2026. latest was 2.2.15 at the time of writing. Behavior changes between versions; check yours.)


Top comments (0)