cabloy 5.1.103 → 5.1.105

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.cabloy-version +1 -1
  2. package/.claude/skills/cabloy-backend-scaffold/references/follow-up-checklist.md +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/CLAUDE.md +1 -0
  5. package/cabloy-docs/backend/controller-aop-guide.md +2 -2
  6. package/cabloy-docs/backend/field-indexes.md +10 -1
  7. package/cabloy-docs/backend/menu-guide.md +30 -0
  8. package/cabloy-docs/frontend/a-model-under-the-hood.md +26 -4
  9. package/cabloy-docs/frontend/model-architecture.md +1 -1
  10. package/cabloy-docs/frontend/model-state-guide.md +122 -16
  11. package/cabloy-docs/frontend/ssr-init-data.md +12 -1
  12. package/cabloy-docs/frontend/use-state-data-best-practices.md +23 -0
  13. package/package.json +1 -1
  14. package/vona/packages-vona/vona/package.json +1 -1
  15. package/vona/pnpm-lock.yaml +31 -31
  16. package/vona/src/suite/a-home/modules/home-user/src/bean/eventListener.activate.ts +5 -4
  17. package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +10 -6
  18. package/vona/src/suite/a-home/modules/home-user/src/config/config.ts +1 -1
  19. package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +7 -7
  20. package/vona/src/suite/a-home/modules/home-user/src/entity/role.ts +4 -0
  21. package/vona/src/suite/a-home/modules/home-user/src/service/passportAdapter.ts +2 -2
  22. package/vona/src/suite/a-home/modules/home-user/src/service/roleAdapter.ts +5 -5
  23. package/vona/src/suite/a-training/modules/training-record/src/bean/ssrMenu.record.ts +1 -0
  24. package/vona/src/suite/a-training/modules/training-student/src/bean/ssrMenu.student.ts +1 -0
  25. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrMenu.home.ts +1 -0
  26. package/vona/src/suite/cabloy-basic/modules/basic-siteadmin/src/bean/ssrSite.admin.ts +5 -1
  27. package/vona/src/suite/cabloy-basic/modules/basic-siteweb/src/bean/ssrSite.web.ts +5 -1
  28. package/vona/src/suite-vendor/a-auth/modules/a-auth/package.json +1 -1
  29. package/vona/src/suite-vendor/a-auth/modules/a-auth/src/service/auth.ts +5 -1
  30. package/vona/src/suite-vendor/a-auth/package.json +1 -1
  31. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/cli/ssrSite/boilerplate/{{sceneName}}.{{beanName}}.ts_ +5 -3
  32. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/package.json +1 -1
  33. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/bean/event.retrieveMenusSite.ts +2 -2
  34. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/lib/beanSsrSiteBase.ts +33 -8
  35. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssr.ts +46 -16
  36. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/service/ssrHandler.ts +2 -4
  37. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/env.ts +1 -0
  38. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrMenu.ts +10 -1
  39. package/vona/src/suite-vendor/a-cabloy/modules/a-ssr/src/types/ssrSite.ts +3 -1
  40. package/vona/src/suite-vendor/a-cabloy/package.json +1 -1
  41. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  42. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/.metadata/index.ts +18 -2
  43. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +6 -1
  44. package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/event.retrievePermissionAction.ts +16 -0
  45. package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
  46. package/vona/src/suite-vendor/a-vona/modules/a-user/src/bean/bean.passport.ts +2 -2
  47. package/vona/src/suite-vendor/a-vona/modules/a-user/src/lib/passport.ts +7 -4
  48. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/passport.ts +1 -1
  49. package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/role.ts +3 -1
  50. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  51. package/zova/env/.env.cabloyBasicAdmin +2 -0
  52. package/zova/env/.env.cabloyBasicWeb +2 -0
  53. package/zova/packages-utils/zova-jsx/package.json +2 -2
  54. package/zova/packages-zova/zova/package.json +3 -3
  55. package/zova/packages-zova/zova-core/package.json +1 -1
  56. package/zova/packages-zova/zova-core/src/core/sys/sys.ts +3 -0
  57. package/zova/packages-zova/zova-core/src/types/utils/env.ts +1 -0
  58. package/zova/src/suite/a-home/modules/home-api/src/api/homeUserPassport.ts +19 -19
  59. package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +11 -10
  60. package/zova/src/suite/a-home/modules/home-api/src/apiSchema/homeUserPassport.ts +5 -5
  61. package/zova/src/suite/a-home/modules/home-base/src/.metadata/page/errorAccessDenied.ts +9 -0
  62. package/zova/src/suite/a-home/modules/home-base/src/page/errorAccessDenied/controller.tsx +40 -0
  63. package/zova/src/suite/a-home/modules/home-base/src/routes.ts +9 -0
  64. package/zova/src/suite/a-home/modules/home-base/src/service/routerGuards.ts +21 -12
  65. package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +7 -1
  66. package/zova/src/suite/a-home/modules/home-passport/src/monkey.ts +17 -3
  67. package/zova/src/suite-vendor/a-zova/modules/a-model/package.json +1 -1
  68. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useState.ts +6 -6
  69. package/zova/src/suite-vendor/a-zova/modules/a-model/src/bean/bean.model/bean.model.useStateGeneral.ts +5 -5
  70. package/zova/src/suite-vendor/a-zova/modules/a-model/src/monkey.ts +11 -2
  71. package/zova/src/suite-vendor/a-zova/modules/a-model/src/types/query.ts +1 -1
  72. package/zova/src/suite-vendor/a-zova/modules/a-router/package.json +1 -1
  73. package/zova/src/suite-vendor/a-zova/modules/a-router/src/monkeySys.ts +10 -1
  74. package/zova/src/suite-vendor/a-zova/modules/a-router/src/types/router.ts +1 -0
  75. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/package.json +1 -1
  76. package/zova/src/suite-vendor/a-zova/modules/a-routertabs/src/model/tabs.ts +4 -4
  77. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  78. package/zova/src/suite-vendor/a-zova/package.json +5 -5
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.103
1
+ 5.1.105
@@ -24,7 +24,7 @@ After generating or extending a backend thread, check which follow-up layers app
24
24
 
25
25
  - migration/version changes
26
26
  - `meta.version`
27
- - field indexes
27
+ - field indexes; in shared-database multitenancy, preserve ordinary lookup indexes and enforce business uniqueness in tenant-aware business logic rather than with `table.unique(...)`
28
28
  - relations
29
29
  - datasource choice
30
30
  - cache behavior
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.105
4
+
5
+ ### Features
6
+
7
+ - Filter SSR menus based on user roles.
8
+ - Add SSR site access policies.
9
+ - Add an event for retrieving action permissions.
10
+ - Update `monkeySys.ts` functionality.
11
+
12
+ ## 5.1.104
13
+
14
+ ### Features
15
+
16
+ - Add update capabilities.
17
+
18
+ ### Bug Fixes
19
+
20
+ - Initialize the passport model during application startup.
21
+
22
+ ### Improvements
23
+
24
+ - Rename the generic async state type.
25
+ - Rename async local state APIs.
26
+ - Document the passport SSR state bridge.
27
+ - Clarify memory state SSR transfer behavior.
28
+ - Clarify how to select model state helpers.
29
+
3
30
  ## 5.1.103
4
31
 
5
32
  ### Features
package/CLAUDE.md CHANGED
@@ -67,6 +67,7 @@ Before inventing a custom implementation path:
67
67
  - `src/bean` defines the global shorthand surface; classes that should not appear in `IBeanRecordGlobal` should move to `src/lib` or `src/service` rather than being filtered by `@Virtual()`.
68
68
  - When backend code references `this.bean.xxx`, `ctx.bean.xxx`, or `app.bean.xxx`, use `IBeanRecordGlobal` and module `src/.metadata/index.ts` as the first static lookup surface; use `IBeanRecordGeneral` or `src/service` only when the target is not a global shorthand.
69
69
  - When adding a persisted field to an existing backend resource, ask the user whether `vonaModule.fileVersion` should be incremented before changing `meta.version.ts` or the module schema path. If yes, add a new migration version and bump `fileVersion`. If no, keep the current `fileVersion` and fold the schema change into the current version path. Do not assume the versioning strategy without confirmation.
70
+ - In shared-database multitenancy, do not use `table.unique(...)` for business uniqueness. Keep ordinary indexes for lookup performance and enforce tenant-scoped uniqueness in the business layer.
70
71
  - For `@Api.field(...)` and related schemaLike composition, framework guards now preserve previously attached OpenAPI metadata across schema rebuilds, but structure-shaping schemaLike is still order-sensitive. Treat `v.object(...)`, `v.array(...)`, `v.optional()`, `v.nullable()`, `v.default(...)`, and preprocess/transform wrappers as structure-shaping; keep the final structure-defining schemaLike last and verify emitted schema/OpenAPI output after such edits.
71
72
 
72
73
  ## Verification expectations
@@ -93,8 +93,8 @@ Typical jobs include:
93
93
  ```typescript
94
94
  @Passport.public()
95
95
  @Passport.activated(false)
96
- @Passport.roleName({ name: 'admin' })
97
- @Passport.admin()
96
+ @Passport.roleName({ name: 'systemAdmin' })
97
+ @Passport.systemAdmin()
98
98
  ```
99
99
 
100
100
  These shorthands still map back to the generic aspect model.
@@ -66,6 +66,14 @@ Field indexes can also be configured through app config.
66
66
 
67
67
  That means index behavior participates in the broader configuration system rather than being locked into one metadata file only.
68
68
 
69
+ ## Shared-database multitenancy and business uniqueness
70
+
71
+ Ordinary indexes in `meta.index` improve lookup and query performance. They do not define a business uniqueness rule.
72
+
73
+ In shared-database multitenancy, do not use `table.unique(...)` for tenant-scoped business uniqueness. A database-wide unique constraint can reject the same business value when it belongs to a different tenant.
74
+
75
+ Keep the ordinary indexes needed by the access pattern, and enforce the tenant-scoped uniqueness rule in tenant-aware business logic.
76
+
69
77
  ## Implementation checks for persistence changes
70
78
 
71
79
  When changing persistence design, do not stop at entity fields and model methods.
@@ -74,7 +82,8 @@ Also ask:
74
82
 
75
83
  1. does this access pattern need an index?
76
84
  2. should the index belong in `meta.index`?
77
- 3. is the typed style a better fit than raw string declarations?
85
+ 3. does a business key need tenant-scoped uniqueness, to be enforced in tenant-aware business logic rather than with `table.unique(...)`?
86
+ 4. is the typed style a better fit than raw string declarations?
78
87
 
79
88
  That leads to backend changes that are more production-aware and more aligned with Vona’s module metadata model.
80
89
 
@@ -55,6 +55,36 @@ This makes menu retrieval part of the broader backend contract surface.
55
55
 
56
56
  In the current repo implementation, the out-of-the-box menu controller is public and delegates directly to `this.scope.service.menu.retrieveMenus(publicPath)`.
57
57
 
58
+ ## Static menu visibility
59
+
60
+ `@SsrMenu(...)` items can declare static role visibility without changing the public menu DTO:
61
+
62
+ ```typescript
63
+ @SsrMenu({
64
+ item: {
65
+ title: $locale('Operations'),
66
+ link: 'presetResource',
67
+ roles: ['systemAdmin'],
68
+ },
69
+ site: 'basic-siteadmin:admin',
70
+ })
71
+ ```
72
+
73
+ - Omit `roles`, or use `roles: []`, to make an item visible to anonymous and authenticated callers.
74
+ - A nonempty `roles` array is visible when the current Passport has at least one matching role name.
75
+ - `roles` is server-only declaration metadata. It is filtered out before the API response and is not part of `IMenuItem`, OpenAPI, or generated frontend clients.
76
+ - This controls navigation disclosure only. It never grants access to a page, controller action, API, or resource; those boundaries retain their own route and Passport/permission guards.
77
+
78
+ SSR Site menu definitions are cached structurally by Site and locale. The framework keeps static role policy in that prepared cache, then creates a filtered response for each request without mutating the cached definition.
79
+
80
+ ### Frontend query lifecycle
81
+
82
+ Passport filtering does not add user or role identity to the frontend menu query key. The menu resource remains keyed by stable inputs: Site/public path and locale.
83
+
84
+ On login, `ModelPassport.afterLogin()` stores the Passport/JWT before returning to the destination layout. The layout's ordinary `$useStateData(...)` lifecycle refreshes stale menu data using the newly authenticated request context. On logout, the Passport model navigates to login and then clears query data.
85
+
86
+ A dynamic role or menu-policy change while the user remains signed in is different: the mutation owner must explicitly refresh authoritative Passport state when needed and invalidate or refetch the affected menu query. This is UI freshness behavior only; route, controller, API, and resource authorization remain enforced independently by their existing guards.
87
+
58
88
  ## Relationship to frontend integration
59
89
 
60
90
  Menu retrieval is especially relevant in SSR-sensitive frontend flows.
@@ -85,9 +85,21 @@ Its main jobs are:
85
85
  - create the shared `QueryClient`
86
86
  - create the `QueryCache`
87
87
  - install the Vue Query plugin into the app
88
- - on the server, dehydrate query state after render
89
- - on the client, hydrate query state during SSR pre-hydration
90
- - clear the query client after server rendering completes
88
+ - on the server, dehydrate eligible query state after render into `stateDefer.query`
89
+ - on the client, hydrate that snapshot during SSR pre-hydration
90
+ - clear the server query client after the snapshot has been created
91
+
92
+ The server-to-client handoff is therefore:
93
+
94
+ ```text
95
+ server QueryClient
96
+ → dehydrate after render
97
+ → stateDefer.query
98
+ → deferred SSR bootstrap state
99
+ → client QueryClient hydrate during SSR pre-hydration
100
+ ```
101
+
102
+ This is an initial SSR handoff, not browser persistence. The server cache is request-scoped for this flow and is cleared after its snapshot is taken.
91
103
 
92
104
  This is the second important lower-level rule:
93
105
 
@@ -200,6 +212,10 @@ A practical reading rule is:
200
212
  - different state families differ in persistence/storage and SSR behavior
201
213
  - but they still participate in one model runtime vocabulary and query-key system
202
214
 
215
+ `$useStateMem(...)` makes the two policy axes especially clear. It returns an assignable custom-ref surface that reads and writes through the Model Query Cache, while setting `persister: false` so no browser storage adapter is used. No persistence backend does not imply no SSR transfer: a successful eligible memory query can still be included in the shared Query Cache snapshot.
216
+
217
+ A hydrated entry is reusable only when the later call resolves to the same effective key. The runtime prefixes each logical key with Model identity and, for selector-enabled Models, selector identity.
218
+
203
219
  ## Persister storage selection and restore/save/remove behavior
204
220
 
205
221
  The persister layer lives in:
@@ -243,7 +259,13 @@ This file confirms default behavior such as:
243
259
  - persistence max-age defaults
244
260
  - dehydration rules for sync persister-backed queries
245
261
 
246
- This is the place to check when the runtime feels surprising even though your page/model code looks correct.
262
+ The current dehydration decision is ordered:
263
+
264
+ 1. `meta.ssr.dehydrate === false` explicitly excludes a query.
265
+ 2. A sync persister-backed query is excluded.
266
+ 3. Remaining queries use TanStack Query's current default dehydration predicate, which accepts successful queries.
267
+
268
+ This is why `$useStateMem(...)` can participate in the initial SSR transfer despite `persister: false`, while `$useStateLocalAsync(...)` opts out explicitly. This is the place to check when the runtime feels surprising even though your page/model code looks correct.
247
269
 
248
270
  ## What this page does not re-explain
249
271
 
@@ -129,7 +129,7 @@ The `a-model` source exposes five main state families through `BeanModelBase` he
129
129
  - `mem` → in-memory state through `$useStateMem(...)`
130
130
  - `local` → local-storage state through `$useStateLocal(...)`
131
131
  - `cookie` → cookie-backed state through `$useStateCookie(...)`
132
- - `db` → async persisted state through `$useStateDb(...)`
132
+ - `db` → async persisted state through `$useStateLocalAsync(...)`
133
133
 
134
134
  This is why Zova Model is better understood as a **unified model-state layer** rather than only a remote-data wrapper.
135
135
 
@@ -86,7 +86,34 @@ The important point is architectural:
86
86
 
87
87
  ## The main helper families
88
88
 
89
- The current source organizes model state around these helper families.
89
+ The current source organizes model state around these helper families. They share model-owned query-key and cache infrastructure, but differ in the surface returned to callers and in their persistence and restore lifecycle.
90
+
91
+ ### Choose the state helper by ownership and lifecycle
92
+
93
+ Choose a helper by the state surface, persistence, restore, and SSR behavior it needs, not only by whether its value should survive a reload.
94
+
95
+ | Helper | Use it when | Caller-facing surface | Persistence and restore |
96
+ | -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
97
+ | `$useStateData(...)` | The model owns remote or query-style async state and consumers need query status | Full query wrapper, including `data`, `error`, and readiness/status state | Normal query lifecycle; may participate in query hydration subject to metadata |
98
+ | `$useStateMem(...)` | The model owns reusable runtime state without a browser persistence backend | Assignable state value | No browser persistence; eligible successful state can transfer through initial SSR hydration |
99
+ | `$useStateLocal(...)` | The model owns a small browser value that should restore synchronously | Assignable state value | Synchronous `localStorage` restore |
100
+ | `$useStateCookie(...)` | The state needs cookie persistence or request-aware handling | Assignable state value | Synchronous cookie restore |
101
+ | `$useStateLocalAsync(...)` | The model owns longer-lived browser-local state whose restore may be asynchronous | Assignable state value | Asynchronous `localforage` restore |
102
+ | `$useStateComputed(...)` | The model exposes derived state | Computed value | No query fetch or persistence |
103
+
104
+ ### Data, local, and async-local behavior
105
+
106
+ `$useStateData(...)`, `$useStateLocal(...)`, and `$useStateLocalAsync(...)` use the same model-owned cache foundation, but they are not interchangeable.
107
+
108
+ | Concern | `$useStateData(...)` | `$useStateLocal(...)` | `$useStateLocalAsync(...)` |
109
+ | -------------------- | --------------------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
110
+ | Return surface | A memoized query wrapper | An assignable state value | An assignable state value |
111
+ | Source or storage | `queryFn`, query cache, and query configuration | `localStorage` | `localforage` |
112
+ | Initial availability | Follows the query lifecycle | Restores synchronously, then falls back to `meta.defaultData` | Restores asynchronously, then falls back to `meta.defaultData` |
113
+ | Intended write flow | Mutation, cache update, and invalidation policy | Replace or assign the model field | Replace or assign the model field |
114
+ | SSR behavior | Can follow ordinary query dehydration and hydration rules | Browser storage is unavailable on the server | Browser storage is unavailable on the server and async-local state explicitly opts out of dehydration |
115
+
116
+ Use the detailed [`$useStateData` Best Practices](/frontend/use-state-data-best-practices) guide for render-versus-interaction placement, `disableSuspenseOnInit`, freshness utilities, and explicit readiness boundaries.
90
117
 
91
118
  ### 1. `$useStateData(...)`
92
119
 
@@ -155,16 +182,62 @@ The pattern to notice is that the model owns both the mutation and the follow-up
155
182
 
156
183
  ## 3. `$useStateMem(...)`
157
184
 
158
- Use this when the state should stay only in memory.
185
+ Use this for reusable runtime state that belongs to a Model but does not need a browser persistence backend. It returns an assignable value, rather than a query wrapper, while retaining Model-owned Query Cache identity.
159
186
 
160
187
  Current-source characteristics:
161
188
 
162
- - no persistence
163
- - `enabled: false`
189
+ - `enabled: false`, so it does not define a normal remote-fetch lifecycle
164
190
  - `staleTime: Infinity`
165
- - still uses model-owned query cache semantics under the hood
191
+ - `meta.persister: false`, so it does not read or write `localStorage`, cookies, or `localforage`
192
+ - reads from existing Query Cache first, then `meta.defaultData`, then `undefined`
193
+ - assignments update the Query Cache without a persister save
194
+ - automatic key prefixing isolates the state by Model identity and, when enabled, selector identity
195
+
196
+ This is useful when several consumers or Model methods need the same business state during the current frontend runtime, but the browser should not restore it from storage. For structured values, replace or assign the Model field so its custom-ref setter updates the Query Cache:
197
+
198
+ ```typescript
199
+ this.selectedIds = nextSelectedIds;
200
+ ```
201
+
202
+ A plain Controller field is usually clearer when the state belongs only to that one Controller. Choose `$useStateLocal(...)`, `$useStateCookie(...)`, or `$useStateLocalAsync(...)` when browser persistence is part of the requirement. Choose `$useStateData(...)` when consumers need query status, errors, freshness, or a remote-fetch lifecycle.
203
+
204
+ ### SSR transfer is not persistence
205
+
206
+ `meta.persister: false` means no browser persistence backend. It does not, by itself, exclude the state from the initial SSR server-to-client Query Cache handoff.
207
+
208
+ After server rendering, a successful eligible memory-state query can be dehydrated with the shared Query Cache. During client SSR pre-hydration, the snapshot is hydrated into the client QueryClient. A later `$useStateMem(...)` read can therefore receive the transferred value directly from cache when its effective key matches: the same Model, the same selector when selector mode is enabled, and the same logical `queryKey`.
209
+
210
+ This transfer has limits:
211
+
212
+ - it applies only to entries accepted by the normal dehydration policy; `meta.ssr.dehydrate: false` opts a state out
213
+ - it is not a replacement for `localStorage`, cookies, or async-local persistence
214
+ - it does not make the value survive a later browser reload without a new SSR handoff
215
+ - it does not make an empty or unsuccessful cache entry transfer automatically
216
+
217
+ The router-tabs Model uses memory state for current navigation keys and for tabs when tab caching is disabled. The page-data Model uses memory slots keyed by page path. Read [SSR Init Data](/frontend/ssr-init-data) for the page-side flow, or [A-Model Under the Hood](/frontend/a-model-under-the-hood) for the Query Cache and dehydration mechanics.
218
+
219
+ ### SSR/CSR bridge: `ModelPassport`
220
+
221
+ `ModelPassport` shows that the state helper selected on the server and the one selected on the client do not have to be the same. Its application initialization creates the same logical state differently by environment:
222
+
223
+ ```typescript
224
+ this.passport = process.env.CLIENT
225
+ ? this.$useStateLocal({ queryKey: ['passport'] })
226
+ : this.$useStateMem({ queryKey: ['passport'] });
227
+ ```
228
+
229
+ On the SSR server, authenticated work can assign the current passport to the memory-backed Query Cache entry. If that entry is successful and eligible, it is dehydrated with the server QueryClient. During client SSR pre-hydration, the snapshot restores the entry before the client Model consumes it.
230
+
231
+ The client then creates a `$useStateLocal(...)` wrapper for the same effective key. Its read order is Query Cache first, then `localStorage`, then `meta.defaultData`. The hydrated server passport therefore wins on the initial client read; synchronous local-storage restoration is a fallback only when the transferred cache entry is absent.
232
+
233
+ This gives authentication state two complementary properties:
234
+
235
+ - SSR uses the request-confirmed passport to keep the initial HTML and client hydration consistent.
236
+ - CSR-only entry and later browser sessions can restore the client-local passport value from `localStorage`.
237
+
238
+ The server `$useStateMem(...)` entry is the entry that is dehydrated. The client `$useStateLocal(...)` call is a new client-side wrapper that reuses hydrated cache because its effective key matches. Matching requires the same Model, the same selector when selector mode is enabled, and the same logical `queryKey`; the short logical key alone is not global identity.
166
239
 
167
- This is useful when you want model ownership and query-key identity without local persistence.
240
+ Reading the hydrated value does not save it back to `localStorage`. A later assignment through the client local-state wrapper updates Query Cache and writes the local-storage record. This is an initial SSR cache handoff plus client persistence fallback, not automatic persistence of the SSR snapshot.
168
241
 
169
242
  ## 4. `$useStateLocal(...)`
170
243
 
@@ -195,9 +268,9 @@ Current-source characteristics:
195
268
 
196
269
  This is particularly relevant for state that must participate in request-aware or SSR-adjacent flows.
197
270
 
198
- ## 6. `$useStateDb(...)`
271
+ ## 6. `$useStateLocalAsync(...)`
199
272
 
200
- Use this when the model state should persist asynchronously in db-style client storage.
273
+ Use this when the model state should persist asynchronously in browser-local storage backed by `localforage`.
201
274
 
202
275
  Current-source characteristics:
203
276
 
@@ -205,10 +278,40 @@ Current-source characteristics:
205
278
  - `enabled: false`
206
279
  - `staleTime: Infinity`
207
280
  - explicit `ssr.dehydrate = false`
208
- - getters may need async restore flow before data is available
281
+ - the first unresolved read may need async restore before the state is available
209
282
 
210
283
  This is useful for larger persisted client state that should outlive a page session without being stored in cookies or plain local storage.
211
284
 
285
+ ### Initialize and ensure async-local state
286
+
287
+ Assign the state property before awaiting it. When later initialization depends on the restored value, establish an explicit restore boundary with `$ensureStateLocalAsync(...)`. It waits for the pending initial restore to settle; it does not force another load and does not guarantee a defined result:
288
+
289
+ ```typescript
290
+ protected async __init__() {
291
+ this.tabs = this.$useStateLocalAsync({
292
+ queryKey: ['tabs'],
293
+ meta: {
294
+ defaultData: [],
295
+ },
296
+ });
297
+
298
+ await this.$ensureStateLocalAsync(this.tabs);
299
+
300
+ // Continue only after persisted tabs, or the fallback default, is available.
301
+ this.initializeTabs();
302
+ }
303
+ ```
304
+
305
+ `meta.defaultData` is a fallback: it initializes the state only when no persisted value is restored. If neither provides a value, the ensured result can still be `undefined`. The router-tabs model follows this assign-then-ensure sequence before it continues with route initialization.
306
+
307
+ Persisted local and async-local state should be updated by replacing or assigning the model field so its custom-ref setter can update query state and persist the new value:
308
+
309
+ ```typescript
310
+ this.tabs = nextTabs;
311
+ ```
312
+
313
+ Do not rely on an in-place nested mutation alone to persist the change. Assigning `undefined` removes the persisted value.
314
+
212
315
  ## 7. `$useStateComputed(...)`
213
316
 
214
317
  Use this when the model should expose derived state that is still model-key-oriented but computed locally.
@@ -233,6 +336,8 @@ that logical key is prefixed internally with model identity, and with selector w
233
336
 
234
337
  That means model code can use short business-facing keys while still getting namespace isolation.
235
338
 
339
+ Automatic model namespacing does not make every varying runtime value part of the logical key. Logical keys still describe stable resource inputs. Do not add a user or role fingerprint solely because a backend response is evaluated through the current Passport; use the normal authentication lifecycle for login/logout and explicitly invalidate or refetch affected stable keys for an in-session policy change. See [use-state-data-best-practices.md](./use-state-data-best-practices.md) for the full rule.
340
+
236
341
  ## Persistence and restore behavior
237
342
 
238
343
  Current source treats persistence as part of the model-state runtime.
@@ -252,16 +357,17 @@ Model state choices can affect SSR behavior.
252
357
 
253
358
  Current-source behaviors to remember:
254
359
 
255
- - query state can be dehydrated on server and hydrated on client
360
+ - successful eligible Query Cache state can be dehydrated on the server and hydrated on the client
361
+ - `$useStateMem(...)` has no browser persister but can participate in that initial SSR handoff unless excluded by metadata
256
362
  - mutations are not dehydrated
257
- - `db` state is explicitly not dehydrated
258
- - server cannot use local-storage or db persistence backends
259
- - cookie state is special because cookie access can still exist through the app cookie surface
363
+ - `$useStateLocalAsync(...)` explicitly opts out of dehydration
364
+ - server-side sync local and cookie persister-backed Query Cache entries are excluded by the default dehydration policy; this does not prevent a client `$useStateLocal(...)` or `$useStateCookie(...)` wrapper from reusing a hydrated entry that the server created with a different eligible helper
365
+ - server cannot use local-storage or async-local persistence backends; cookie state is special because cookie access can still exist through the app cookie surface
260
366
 
261
367
  Practical implication:
262
368
 
263
- - do not assume every persisted model state behaves the same during SSR
264
- - choose `mem`, `local`, `cookie`, `db`, or `data` based on ownership and hydration requirements, not only convenience
369
+ - do not equate browser persistence with SSR transfer, or assume every state family behaves the same during SSR
370
+ - choose `mem`, `local`, `cookie`, `localAsync`, or `data` based on ownership, persistence, and hydration requirements, not only convenience
265
371
 
266
372
  ## Real examples to read
267
373
 
@@ -289,7 +395,7 @@ This file shows:
289
395
 
290
396
  - `@Model({ enableSelector: true, ... })`
291
397
  - richer model initialization in `__init__`
292
- - mixed use of `$useStateMem(...)` and `$useStateDb(...)`
398
+ - mixed use of `$useStateMem(...)` and `$useStateLocalAsync(...)`
293
399
  - model-owned tab state with cache and persistence decisions
294
400
 
295
401
  ### SSR-sensitive auth model
@@ -31,7 +31,18 @@ export class ControllerPageTodo {
31
31
  1. a model bean encapsulates the data access path
32
32
  2. the controller injects the model
33
33
  3. `__init__` prepares the data on the server
34
- 4. model-based SSR support synchronizes that data to the client and completes hydration automatically
34
+ 4. successful eligible Query Cache state is captured in the SSR handoff
35
+ 5. client SSR pre-hydration restores that cache before the client model consumes it
36
+
37
+ ## What hydration reuses
38
+
39
+ The initial SSR handoff reuses model Query Cache data, not only `$useStateData(...)` results. A successful eligible `$useStateMem(...)` value can also transfer from the server to the initial client runtime even though it has no browser persistence backend.
40
+
41
+ Client reuse requires the model call to resolve to the same effective query key: Zova prefixes the logical key with Model identity and, for selector-enabled models, selector identity. State marked with `meta.ssr.dehydrate: false` is excluded. This handoff does not make memory state durable across a later browser reload, and it does not guarantee that every client query avoids fetching.
42
+
43
+ `ModelPassport` is a concrete SSR/CSR bridge: the server creates `passport` with `$useStateMem({ queryKey: ['passport'] })`, while the client creates `$useStateLocal({ queryKey: ['passport'] })`. The successful eligible server memory entry is what SSR transfers. Because the client local-state wrapper reads Query Cache before `localStorage`, the hydrated passport supplies the initial client value when the effective key matches. If no transferred entry exists, `$useStateLocal(...)` can instead restore its browser-local value. Hydration alone does not save the transferred value to `localStorage`; later client-side assignments do.
44
+
45
+ For helper selection and the complete passport flow, read [Model State Guide](/frontend/model-state-guide). For the dehydration filter and QueryClient lifecycle, read [A-Model Under the Hood](/frontend/a-model-under-the-hood).
35
46
 
36
47
  ## Implementation checks for SSR data-loading changes
37
48
 
@@ -325,6 +325,29 @@ Readable keys improve:
325
325
  - tracing cache behavior
326
326
  - future refactors when similar query families grow
327
327
 
328
+ ## Practical rule 9: key stable resource inputs, not ambient caller identity
329
+
330
+ A `$useStateData(...)` query key identifies the stable variant of a resource. Include inputs that change that variant, such as an operation or resource identifier, filters, pagination, Site/public path, or locale.
331
+
332
+ Do not append the current username, authenticated flag, Passport role names or IDs, or another authorization fingerprint merely because the server evaluates the current Passport while producing the response. Those values are ambient request context, not automatically part of the frontend resource identity.
333
+
334
+ Normal authentication transitions already provide lifecycle boundaries: login stores the new Passport before navigating into the destination model, and that model's ordinary query lifecycle refreshes stale data under the new request context. Logout clears query data after leaving protected UI.
335
+
336
+ Use explicit invalidation or refetch when authorization policy changes during an otherwise continuing authenticated session, such as an administrator changing role membership, Site policy, or menu policy. Refresh authoritative Passport state when necessary, then synchronize the affected stable resource key; do not turn a policy fingerprint into the key by default.
337
+
338
+ For example, the menu resource is keyed by its stable Site/public path and locale:
339
+
340
+ ```ts
341
+ queryKey: ['retrieveMenus', publicPath, locale];
342
+ ```
343
+
344
+ Do not key it by the current roles:
345
+
346
+ ```ts
347
+ // Avoid: roles are ambient request policy, not menu resource identity.
348
+ queryKey: ['retrieveMenus', publicPath, locale, roleNames];
349
+ ```
350
+
328
351
  ## Anti-pattern: command-first controller state
329
352
 
330
353
  A common anti-pattern looks like this:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.103",
3
+ "version": "5.1.105",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.73",
3
+ "version": "5.1.74",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -501,8 +501,8 @@ importers:
501
501
  specifier: npm:@cabloy/zod@4.3.8
502
502
  version: '@cabloy/zod@4.3.8'
503
503
  zova:
504
- specifier: ^5.1.132
505
- version: 5.1.132(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
504
+ specifier: ^5.1.134
505
+ version: 5.1.134(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
506
506
  zova-jsx:
507
507
  specifier: ^1.1.77
508
508
  version: 1.1.77(typescript@5.9.3)
@@ -528,8 +528,8 @@ importers:
528
528
  specifier: ^5.1.29
529
529
  version: 5.1.29
530
530
  zova-module-a-model:
531
- specifier: ^5.1.31
532
- version: 5.1.31(vue@3.5.39(typescript@5.9.3))
531
+ specifier: ^5.1.33
532
+ version: 5.1.33(vue@3.5.39(typescript@5.9.3))
533
533
  zova-module-a-openapi:
534
534
  specifier: ^5.1.43
535
535
  version: 5.1.43
@@ -537,8 +537,8 @@ importers:
537
537
  specifier: ^5.1.29
538
538
  version: 5.1.29
539
539
  zova-module-a-routertabs:
540
- specifier: ^5.1.32
541
- version: 5.1.32
540
+ specifier: ^5.1.33
541
+ version: 5.1.33
542
542
  zova-module-a-ssr:
543
543
  specifier: ^5.1.29
544
544
  version: 5.1.29
@@ -573,8 +573,8 @@ importers:
573
573
  specifier: npm:@cabloy/zod@4.3.8
574
574
  version: '@cabloy/zod@4.3.8'
575
575
  zova:
576
- specifier: ^5.1.132
577
- version: 5.1.132(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
576
+ specifier: ^5.1.134
577
+ version: 5.1.134(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
578
578
  zova-jsx:
579
579
  specifier: ^1.1.77
580
580
  version: 1.1.77(typescript@5.9.3)
@@ -600,8 +600,8 @@ importers:
600
600
  specifier: ^5.1.29
601
601
  version: 5.1.29
602
602
  zova-module-a-model:
603
- specifier: ^5.1.31
604
- version: 5.1.31(vue@3.5.39(typescript@5.9.3))
603
+ specifier: ^5.1.33
604
+ version: 5.1.33(vue@3.5.39(typescript@5.9.3))
605
605
  zova-module-a-openapi:
606
606
  specifier: ^5.1.43
607
607
  version: 5.1.43
@@ -609,8 +609,8 @@ importers:
609
609
  specifier: ^5.1.29
610
610
  version: 5.1.29
611
611
  zova-module-a-routertabs:
612
- specifier: ^5.1.32
613
- version: 5.1.32
612
+ specifier: ^5.1.33
613
+ version: 5.1.33
614
614
  zova-module-a-ssr:
615
615
  specifier: ^5.1.29
616
616
  version: 5.1.29
@@ -1517,13 +1517,13 @@ importers:
1517
1517
  src/suite-vendor/a-file:
1518
1518
  dependencies:
1519
1519
  vona-module-a-file:
1520
- specifier: ^5.0.4
1520
+ specifier: ^5.0.5
1521
1521
  version: link:modules/a-file
1522
1522
  vona-module-file-cloudflare:
1523
- specifier: ^5.0.3
1523
+ specifier: ^5.0.4
1524
1524
  version: link:modules/file-cloudflare
1525
1525
  vona-module-file-native:
1526
- specifier: ^5.0.3
1526
+ specifier: ^5.0.4
1527
1527
  version: link:modules/file-native
1528
1528
 
1529
1529
  src/suite-vendor/a-file/modules/a-file:
@@ -1625,7 +1625,7 @@ importers:
1625
1625
  src/suite-vendor/a-test:
1626
1626
  dependencies:
1627
1627
  vona-module-test-file:
1628
- specifier: ^5.0.4
1628
+ specifier: ^5.0.5
1629
1629
  version: link:modules/test-file
1630
1630
 
1631
1631
  src/suite-vendor/a-test/modules/test-auth:
@@ -7993,8 +7993,8 @@ packages:
7993
7993
  zova-module-a-meta@5.1.21:
7994
7994
  resolution: {integrity: sha512-7vsLOMw1UQEZ+8qbBaiz9t4n3N7AU1jxpaiSIIky7hBvKO9nuHq+e7FONpj9poa8WeHkYoflBg/8imKBy/IlNw==}
7995
7995
 
7996
- zova-module-a-model@5.1.31:
7997
- resolution: {integrity: sha512-/6MV5xSklEkain5t0vsZ37ImyIihutCu7ymytq5RFG0QbeKtbSAI/IB6TS2mfkjLkiy97SmDJbuiLGso7Cg/Gw==}
7996
+ zova-module-a-model@5.1.33:
7997
+ resolution: {integrity: sha512-NwxCguvESPLUSA2zo5vVH3Qx29o5znmUL2qU9khwjXUmDPbOTeYTkgPCI33fTsy8VnkguoWBLMgUdOsaJF+7OA==}
7998
7998
 
7999
7999
  zova-module-a-openapi@5.1.43:
8000
8000
  resolution: {integrity: sha512-xJKvSPl1AZnH7EpgEd9a3u3IEpJfnou19DKre/7TL1KdiTdfNThLrmmkdP1UNYlUHUgCczcGUwZDdMiE9gdxwA==}
@@ -8005,8 +8005,8 @@ packages:
8005
8005
  zova-module-a-routerstack@5.1.26:
8006
8006
  resolution: {integrity: sha512-uu4tFw2DUruanPR7cojM5p6rvyCIh+tnwPmq7s0jZdg+5XBr+YMy1RGjnE8vPFSLHW8C71hlNbps6VpKvKMiiQ==}
8007
8007
 
8008
- zova-module-a-routertabs@5.1.32:
8009
- resolution: {integrity: sha512-HZziZYnXUAZZ3YhQl2Qc+/KhCS7/GlKs9Ob4eIZok+YdDPF3iaVJsW+y+5l3rbtxIFsZtRsUZtoys2+p5wEvSQ==}
8008
+ zova-module-a-routertabs@5.1.33:
8009
+ resolution: {integrity: sha512-u5YIFc7OfUsxruIG8QP4sH1+TCps2vAYUpAMNfT1XPkJLHdFGkjEc6++KMCpv4jGpNDoQ42/LefEQGg7vqnQuw==}
8010
8010
 
8011
8011
  zova-module-a-ssr@5.1.29:
8012
8012
  resolution: {integrity: sha512-wlxLSy+Wjh/UFEVqsKQ105bvXceXPdIDmJQPKF0hSwiNY1R8HUottADfXPwYOatzZAayZ4AU4vQIpG92F2GJiA==}
@@ -8032,11 +8032,11 @@ packages:
8032
8032
  zova-module-rest-resource@5.1.41:
8033
8033
  resolution: {integrity: sha512-7Y8rLmHdw0pynlWqsyHPWuCWFLwr/0HsNvfef64cPw91LRujf/7OlulVWqH8ZoFNioZZUpfumB+t/+mICoYfFg==}
8034
8034
 
8035
- zova-suite-a-zova@5.1.131:
8036
- resolution: {integrity: sha512-NRPt3ahLwHg+rLuA2VXRbEmrg/bTdgwdQmU9C8nj9R0+E1f0ehbbsUWJFQFN6hJNco5+rDDSDfBlvejf4z6L1w==}
8035
+ zova-suite-a-zova@5.1.133:
8036
+ resolution: {integrity: sha512-N7YSmamP2eV3MBV4RnFXXUlDg9yEcvYkHDxfcgQQFjR1iQyxNbCLO1Ud4Bjcinray2KNrnOF41e+0gn7A6jzRQ==}
8037
8037
 
8038
- zova@5.1.132:
8039
- resolution: {integrity: sha512-lQK1VRnwthDx0qayS7kdElyfAmvPEhlt6sk/2jK5mTbqojsjuDNkrEa9Ck2Q78fag800U6pDwV83o6CRbSUTeA==}
8038
+ zova@5.1.134:
8039
+ resolution: {integrity: sha512-nnieXjXy0+94FKZCT05dGvwASJQ7PiM387r+dToLAuSFRCqgQwaLvYyrOXN2RhKt8Z5nefWDMwA9LPWgBksnLg==}
8040
8040
 
8041
8041
  zwitch@1.0.5:
8042
8042
  resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==}
@@ -13935,7 +13935,7 @@ snapshots:
13935
13935
 
13936
13936
  zova-module-a-meta@5.1.21: {}
13937
13937
 
13938
- zova-module-a-model@5.1.31(vue@3.5.39(typescript@5.9.3)):
13938
+ zova-module-a-model@5.1.33(vue@3.5.39(typescript@5.9.3)):
13939
13939
  dependencies:
13940
13940
  '@tanstack/query-core': 5.101.2
13941
13941
  '@tanstack/query-persist-client-core': 5.101.2
@@ -13957,7 +13957,7 @@ snapshots:
13957
13957
 
13958
13958
  zova-module-a-routerstack@5.1.26: {}
13959
13959
 
13960
- zova-module-a-routertabs@5.1.32: {}
13960
+ zova-module-a-routertabs@5.1.33: {}
13961
13961
 
13962
13962
  zova-module-a-ssr@5.1.29:
13963
13963
  dependencies:
@@ -14010,7 +14010,7 @@ snapshots:
14010
14010
 
14011
14011
  zova-module-rest-resource@5.1.41: {}
14012
14012
 
14013
- zova-suite-a-zova@5.1.131(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14013
+ zova-suite-a-zova@5.1.133(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14014
14014
  dependencies:
14015
14015
  zova-module-a-api: 5.1.21
14016
14016
  zova-module-a-app: 5.1.24
@@ -14025,11 +14025,11 @@ snapshots:
14025
14025
  zova-module-a-interceptor: 5.1.29
14026
14026
  zova-module-a-logger: 5.1.26
14027
14027
  zova-module-a-meta: 5.1.21
14028
- zova-module-a-model: 5.1.31(vue@3.5.39(typescript@5.9.3))
14028
+ zova-module-a-model: 5.1.33(vue@3.5.39(typescript@5.9.3))
14029
14029
  zova-module-a-openapi: 5.1.43
14030
14030
  zova-module-a-router: 5.1.29
14031
14031
  zova-module-a-routerstack: 5.1.26
14032
- zova-module-a-routertabs: 5.1.32
14032
+ zova-module-a-routertabs: 5.1.33
14033
14033
  zova-module-a-ssr: 5.1.29
14034
14034
  zova-module-a-ssrhmr: 5.1.22
14035
14035
  zova-module-a-ssrserver: 5.1.22
@@ -14044,10 +14044,10 @@ snapshots:
14044
14044
  - typescript
14045
14045
  - vue
14046
14046
 
14047
- zova@5.1.132(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14047
+ zova@5.1.134(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3)):
14048
14048
  dependencies:
14049
14049
  zova-core: 5.1.77(patch_hash=0f4164c4d2bbb16d671beb5c26759dff37769be0709df941bc7b90b94261ac54)(typescript@5.9.3)
14050
- zova-suite-a-zova: 5.1.131(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14050
+ zova-suite-a-zova: 5.1.133(typescript@5.9.3)(vue@3.5.39(typescript@5.9.3))
14051
14051
  transitivePeerDependencies:
14052
14052
  - '@vue/composition-api'
14053
14053
  - debug