@jskit-ai/agent-docs 0.1.283 → 0.1.285

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 (152) hide show
  1. package/guide/agent/app-extras/assistant.md +14 -2
  2. package/guide/agent/app-extras/realtime.md +38 -1
  3. package/guide/agent/app-setup/authentication.md +76 -14
  4. package/guide/agent/app-setup/console.md +8 -0
  5. package/guide/agent/app-setup/database-layer.md +23 -103
  6. package/guide/agent/app-setup/existing-application-migration.md +134 -72
  7. package/guide/agent/app-setup/multi-homing.md +94 -32
  8. package/guide/agent/app-setup/quickstart.md +1 -1
  9. package/guide/agent/app-setup/run-an-accounts-hub.md +1 -1
  10. package/guide/agent/app-setup/upgrading-jskit.md +17 -3
  11. package/guide/agent/app-setup/users.md +8 -5
  12. package/guide/agent/framework/application-operations.md +1 -1
  13. package/guide/agent/framework/crud-operations.md +63 -45
  14. package/guide/agent/framework/data-access.md +1207 -0
  15. package/guide/agent/framework/sanity-checks.md +26 -27
  16. package/guide/agent/framework/typed-features.md +117 -248
  17. package/guide/agent/index.md +2 -0
  18. package/package.json +6 -3
  19. package/patterns/feature-package/PATTERN.md +25 -17
  20. package/patterns/feature-package/example/booking-engine/package.json +1 -1
  21. package/patterns/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  22. package/patterns/feature-package/example/variations/invoiceResource.js +15 -0
  23. package/patterns/feature-package/example/variations/invoiceRollupActions.js +4 -4
  24. package/patterns/feature-package/example/variations/ownedTableRepository.js +27 -0
  25. package/patterns/feature-package/example/variations/types.js +1 -1
  26. package/patterns/minimal-foundation/example/AGENTS.md +4 -0
  27. package/patterns/minimal-foundation/example/package.json +4 -4
  28. package/patterns/minimal-foundation/example/server.js +4 -1
  29. package/patterns/shell-foundation/example/AGENTS.md +4 -0
  30. package/patterns/shell-foundation/example/package.json +5 -5
  31. package/patterns/shell-foundation/example/packages/main/package.json +1 -1
  32. package/patterns/shell-foundation/example/server.js +4 -1
  33. package/reference/autogen/KERNEL_MAP.md +3 -1
  34. package/reference/autogen/PATTERN_INDEX.md +70 -59
  35. package/reference/autogen/README.md +1 -1
  36. package/reference/autogen/packages/agent-docs.md +7 -3
  37. package/reference/autogen/packages/agent-runtime.md +48 -11
  38. package/reference/autogen/packages/architecture-inspector.md +6 -2
  39. package/reference/autogen/packages/assistant-core.md +4 -4
  40. package/reference/autogen/packages/assistant-runtime.md +33 -13
  41. package/reference/autogen/packages/auth-provider-local-core.md +3 -1
  42. package/reference/autogen/packages/auth-provider-local-db-core.md +69 -14
  43. package/reference/autogen/packages/auth-provider-oidc-core.md +5 -1
  44. package/reference/autogen/packages/auth-provider-supabase-core.md +1 -1
  45. package/reference/autogen/packages/connectors-core.md +22 -2
  46. package/reference/autogen/packages/console-core.md +9 -5
  47. package/reference/autogen/packages/crud-core.md +45 -175
  48. package/reference/autogen/packages/database-runtime-mysql.md +0 -26
  49. package/reference/autogen/packages/database-runtime.md +11 -33
  50. package/reference/autogen/packages/http-runtime.md +3 -3
  51. package/reference/autogen/packages/http-web.md +12 -0
  52. package/reference/autogen/packages/kernel.md +27 -2
  53. package/reference/autogen/packages/oidc-issuer-core.md +4 -0
  54. package/reference/autogen/packages/payments-core.md +28 -6
  55. package/reference/autogen/packages/realtime.md +15 -6
  56. package/reference/autogen/packages/resource-core.md +32 -5
  57. package/reference/autogen/packages/resource-crud-core.md +30 -34
  58. package/reference/autogen/packages/resource-runtime.md +220 -0
  59. package/reference/autogen/packages/rewarded-core.md +16 -10
  60. package/reference/autogen/packages/shell-web.md +1 -0
  61. package/reference/autogen/packages/users-core.md +33 -14
  62. package/reference/autogen/packages/workspaces-core.md +17 -10
  63. package/reference/autogen/tooling/config-eslint.md +74 -0
  64. package/reference/autogen/tooling/jskit-catalog.md +50 -1
  65. package/reference/autogen/tooling/jskit-release.md +5 -0
  66. package/skills/jskit/SKILL.md +4 -1
  67. package/skills/jskit/references/app-operations.md +1 -1
  68. package/skills/jskit/references/crud-operations.md +62 -44
  69. package/skills/jskit/references/data-access.md +1202 -0
  70. package/skills/jskit/references/database-layer.md +411 -0
  71. package/skills/jskit/references/existing-application-migration.md +134 -72
  72. package/skills/jskit/references/heuristics/INDEX.md +6 -4
  73. package/skills/jskit/references/heuristics/crud-authoring.md +26 -15
  74. package/skills/jskit/references/heuristics/data-access.md +19 -0
  75. package/skills/jskit/references/heuristics/filters.md +6 -6
  76. package/skills/jskit/references/heuristics/row-policies.md +40 -16
  77. package/skills/jskit/references/heuristics/server-search.md +30 -42
  78. package/skills/jskit/references/heuristics/table-resource-fields.md +78 -0
  79. package/skills/jskit/references/heuristics/typed-features.md +3 -3
  80. package/skills/jskit/references/multi-homing.md +94 -32
  81. package/skills/jskit/references/pattern-index.md +70 -59
  82. package/skills/jskit/references/patterns/app/minimal-foundation/example/AGENTS.md +4 -0
  83. package/skills/jskit/references/patterns/app/minimal-foundation/example/package.json +4 -4
  84. package/skills/jskit/references/patterns/app/minimal-foundation/example/server.js +4 -1
  85. package/skills/jskit/references/patterns/app/shell-foundation/example/AGENTS.md +4 -0
  86. package/skills/jskit/references/patterns/app/shell-foundation/example/package.json +5 -5
  87. package/skills/jskit/references/patterns/app/shell-foundation/example/packages/main/package.json +1 -1
  88. package/skills/jskit/references/patterns/app/shell-foundation/example/server.js +4 -1
  89. package/skills/jskit/references/patterns/assistant/assistant-surface/PATTERN.md +5 -2
  90. package/skills/jskit/references/patterns/auth/accounts-hub/example/package.json +3 -2
  91. package/skills/jskit/references/patterns/auth/auth-surface/PATTERN.md +15 -0
  92. package/skills/jskit/references/patterns/auth/oidc-sign-in/example/package.json +2 -2
  93. package/skills/jskit/references/patterns/auth/supabase-auth/example/package.json +1 -1
  94. package/skills/jskit/references/patterns/connectors/calendar-cli/example/package.json +4 -4
  95. package/skills/jskit/references/patterns/crud/crud-contract/PATTERN.md +133 -0
  96. package/skills/jskit/references/patterns/crud/crud-contract/example/bookResource.js +74 -0
  97. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/PATTERN.md +69 -37
  98. package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/package.json +5 -4
  99. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/server/BooksFeature.js +13 -0
  100. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/bookResource.js +74 -0
  101. package/skills/jskit/references/patterns/crud/crud-feature-package/example/packages/books/src/shared/index.js +1 -0
  102. package/skills/jskit/references/patterns/crud/crud-screen-set/PATTERN.md +26 -8
  103. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookEditPage.vue +2 -2
  104. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookListPage.vue +2 -2
  105. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookNewPage.vue +2 -2
  106. package/skills/jskit/references/patterns/crud/crud-screen-set/example/books/BookViewPage.vue +3 -3
  107. package/skills/jskit/references/patterns/database/mysql-application/PATTERN.md +9 -4
  108. package/skills/jskit/references/patterns/database/mysql-application/example/package.json +2 -1
  109. package/skills/jskit/references/patterns/database/mysql-application/example/scripts/prepare-database.js +8 -0
  110. package/skills/jskit/references/patterns/database/postgres-application/PATTERN.md +8 -4
  111. package/skills/jskit/references/patterns/database/postgres-application/example/package.json +2 -1
  112. package/skills/jskit/references/patterns/database/postgres-application/example/scripts/prepare-database.js +8 -0
  113. package/skills/jskit/references/patterns/database/table-resource/PATTERN.md +120 -0
  114. package/skills/jskit/references/patterns/database/table-resource/example/orderResource.js +25 -0
  115. package/skills/jskit/references/patterns/realtime/realtime-application/PATTERN.md +7 -0
  116. package/skills/jskit/references/patterns/realtime/realtime-application/example/package.json +2 -2
  117. package/skills/jskit/references/patterns/rewards/google-rewarded/PATTERN.md +1 -1
  118. package/skills/jskit/references/patterns/server/feature-package/PATTERN.md +25 -17
  119. package/skills/jskit/references/patterns/server/feature-package/example/booking-engine/package.json +1 -1
  120. package/skills/jskit/references/patterns/server/feature-package/example/variations/InvoiceRollupFeature.js +6 -7
  121. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceResource.js +15 -0
  122. package/skills/jskit/references/patterns/server/feature-package/example/variations/invoiceRollupActions.js +4 -4
  123. package/skills/jskit/references/patterns/server/feature-package/example/variations/ownedTableRepository.js +27 -0
  124. package/skills/jskit/references/patterns/server/feature-package/example/variations/types.js +1 -1
  125. package/skills/jskit/references/patterns/users/user-administration-server/PATTERN.md +63 -10
  126. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/package.json +5 -4
  127. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/server/UsersFeature.js +7 -5
  128. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/index.js +1 -1
  129. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userCrud.js +21 -0
  130. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/package.json +11 -5
  131. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/UsersWorkspaceFeature.js +14 -5
  132. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/server/workspaceMembersOnly.js +52 -0
  133. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/index.js +1 -1
  134. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userCrud.js +21 -0
  135. package/skills/jskit/references/patterns/workspaces/workspace-server/PATTERN.md +9 -9
  136. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/PATTERN.md +16 -0
  137. package/skills/jskit/references/patterns/workspaces/workspace-surfaces/example/src/pages/home/workspaces/index.vue +2 -0
  138. package/skills/jskit/references/sanity-checks.md +27 -28
  139. package/skills/jskit/references/typed-features.md +117 -248
  140. package/patterns/feature-package/example/variations/customKnexRepository.js +0 -40
  141. package/reference/autogen/packages/json-rest-api-core.md +0 -66
  142. package/skills/jskit/references/heuristics/crud-repository-mapping.md +0 -140
  143. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/server/BooksFeature.js +0 -13
  144. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/bookResource.js +0 -83
  145. package/skills/jskit/references/patterns/crud/json-api-resource-package/example/packages/books/src/shared/index.js +0 -1
  146. package/skills/jskit/references/patterns/crud/resource-contract/PATTERN.md +0 -155
  147. package/skills/jskit/references/patterns/crud/resource-contract/example/bookResource.js +0 -87
  148. package/skills/jskit/references/patterns/server/feature-package/example/variations/customKnexRepository.js +0 -40
  149. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users/src/shared/userResource.js +0 -69
  150. package/skills/jskit/references/patterns/users/user-administration-server/example/packages/users-workspace/src/shared/userResource.js +0 -56
  151. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/migrations/20260815000000_books.cjs +0 -0
  152. /package/skills/jskit/references/patterns/crud/{json-api-resource-package → crud-feature-package}/example/packages/books/src/server/types.js +0 -0
@@ -101,31 +101,33 @@ application's normal verification script, and Playwright.
101
101
 
102
102
  ## 3. Choose the narrowest server seam that owns each behavior
103
103
 
104
- Conventional resources use `defineCrudResource()` and
105
- `defineCrudJsonApiFeature()` so JSKIT owns the repeated repository, service,
106
- permission, action, JSON API, and route mechanics. Extend that contract instead
104
+ Conventional resources use `defineTableResource()`, `defineCrudContract()`
105
+ and `defineCrudFeature()` so JSKIT owns the repeated repository,
106
+ service, permission, action, JSON API, and route mechanics. Extend that contract instead
107
107
  of copying it.
108
108
 
109
109
  | Product need | Normal owner |
110
110
  | --- | --- |
111
111
  | Behavior before, around, or after one standard CRUD operation | `operationLifecycle` |
112
112
  | A resource-specific domain method or override | `decorateService` |
113
- | A resource-specific query, lock, or persistence operation | `decorateRepository` |
113
+ | A resource-specific query, lock, or many-row write | `decorateRepository`, on the table's handle |
114
114
  | A named non-CRUD command on the resource | a named CRUD `action` backed by the service |
115
115
  | A separate aggregate, workflow, or capability domain | `defineFeature()` with named actions |
116
- | Persistence that the high-level resource APIs cannot express | an explicit custom repository owned by that Feature |
116
+ | A report the handle cannot express | one `scopedQuery()` repository method, under `// Knex: <kind> - <reason>` ([data access](/guide/framework/data-access#layer-5-reports-the-resource-cannot-express)) |
117
117
 
118
118
  For mutations, lifecycle phases before commit share one repository
119
119
  transaction. An `execute` hook that only normalizes input calls
120
- `standard(nextInput)` rather than reimplementing the write. Repositories own
121
- database access; services and hooks orchestrate repositories. External work
120
+ `standard(nextInput)` rather than reimplementing the write. The feature that
121
+ owns a table reads and writes it through the handles its `owns` entry gives
122
+ its repository; services, hooks and other features call that repository or
123
+ the owner's API, never the table. External work
122
124
  runs after commit, or a durable outbox record is written inside the transaction
123
125
  and delivered separately.
124
126
 
125
127
  Give commands product names such as `confirm`, `publish`, or `cancel`, with
126
128
  explicit input, permissions, routes where needed, audit behavior, events, and
127
129
  tests. Do not hide unrelated workflows behind a generic `execute(anything)`
128
- action, and do not create a custom repository merely because SQL is familiar.
130
+ action, and do not write Knex merely because SQL is familiar.
129
131
 
130
132
  ### Reuse response serializers
131
133
 
@@ -148,9 +150,11 @@ upgrading JSKIT and declaring the directly used
148
150
  ```js
149
151
  import Fastify from "fastify";
150
152
  import SerializerSelector from "@fastify/fast-json-stringify-compiler";
151
- import { createCachedResponseSerializerFactory } from "@jskit-ai/kernel/server/http";
153
+ import { createCachedResponseSerializerFactory, resolveTrustedProxies } from "@jskit-ai/kernel/server/http";
152
154
 
153
155
  const app = Fastify({
156
+ // TRUSTED_PROXIES: the proxies whose X-Forwarded-For is believed (loopback without it).
157
+ trustProxy: resolveTrustedProxies(runtimeEnv),
154
158
  schemaController: {
155
159
  compilersFactory: {
156
160
  buildSerializer: createCachedResponseSerializerFactory(SerializerSelector())
@@ -160,84 +164,142 @@ const app = Fastify({
160
164
  ```
161
165
 
162
166
  Keep the application's other Fastify options, including serializer options and
163
- validator configuration. This setup wraps the native default serializer builder;
167
+ validator configuration, but replace an application's own `trustProxy` setting
168
+ with `resolveTrustedProxies(runtimeEnv)` (`runtimeEnv` is the environment the
169
+ server starts with, `process.env` if it has no other), and its environment
170
+ variable with `TRUSTED_PROXIES`; see
171
+ [sign-in protection](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/authentication#sign-in-protection)
172
+ for the setting. This setup wraps the native default serializer builder;
164
173
  retain any deliberately chosen custom compiler instead of replacing or wrapping
165
174
  it. Native route-level compiler overrides continue to apply. Update the lockfile
166
175
  through the application's normal installation flow and restart the backend to
167
176
  activate the new setup. Updating packages or starter examples alone does not
168
177
  change an existing application's startup source.
169
178
 
170
- ### JSON REST v2 integration
179
+ ### Data access on table resources (json-rest-api 3)
171
180
 
172
- Applications using `@jskit-ai/json-rest-api-core` require Node.js 24 or newer.
181
+ Applications using `@jskit-ai/resource-runtime` require Node.js 24 or newer.
173
182
  JSKIT's repository and application default is Node.js 26. Upgrade dependencies,
174
- custom resource calls, and hooks together; rebuild native database dependencies
175
- after changing Node.
176
-
177
- The standard `defineCrudJsonApiFeature()` API keeps `queryDocuments`,
178
- `getDocumentById`, `createDocument`, `patchDocumentById`, and
179
- `deleteDocumentById`, with `{ trx, context }` repository/service options.
180
- Read, create, and patch services still return tagged JSON:API documents.
181
- Application-owned hosts use `JsonRestApi` from `json-rest-api`; the JSKIT host
182
- already owns that setup.
183
-
184
- For direct resource calls, replace `inputRecord` with exactly one of `data`
185
- (plain values) or `document` (a JSON:API document). `format` selects output
186
- independently: `"plain"` or `"jsonapi"`. Replace `simplified` and
187
- `returnFullRecord` with `format` and `returning: "full" | "minimal" | "none"`.
188
- The JSKIT host defaults to plain output and full write responses; standard
189
- document repositories explicitly request JSON:API output and full create/patch
190
- responses. A plain collection is still `{ data: [...], meta?, links? }`; use
191
- `extractJsonRestCollectionRows()` when a custom repository needs its rows.
192
- A query without a page returns only the resource's first 20 rows; a
193
- server-side read that needs every row uses `queryAllJsonRestCollectionRows()`
194
- ([every row](/guide/framework/typed-features#every-row)).
183
+ resource calls and hooks together; rebuild native database dependencies after
184
+ changing Node.
195
185
 
196
- ```js
197
- await jsonRestApi.resources.books.patch({
198
- id: recordId,
199
- data: { title: "Revised title" },
200
- format: "jsonapi",
201
- returning: "full",
202
- transaction: trx
203
- }, createJsonRestContext(context));
204
- ```
186
+ Every table has one table resource and one owner feature, and every read and
187
+ write goes through that table's handle
188
+ ([data access](/guide/framework/data-access)). Rename as you go:
205
189
 
206
- Keep the target ID outside the patch data. Direct plain writes use the
207
- belongs-to relationship name and ID, such as `author: authorId`; the standard
208
- repository maps a resource field's declared `as` alias. Do not send both names.
209
-
210
- Resource hooks share the mutable context supplied as the second argument
211
- within one operation. The normalized hook field remains
212
- `context.inputRecord.data`; only the public input argument changed.
213
- `createJsonRestContext()` clones the JSKIT execution context and its
214
- `visibilityContext` and `scopeValues`, allowing frozen caller contexts; other
215
- nested objects remain shared references. Permission hooks use
216
- `context.originalContext ?? context` for operation-wide caches; enrichment
217
- hooks use `context.parentContext`. Built-in nested operations get separate
218
- contexts with shallow inheritance. For an application-initiated child call,
219
- choose the application properties to share in a fresh context and pass
220
- `transaction` explicitly; reusing or blindly spreading an active operation
221
- context carries its working state into the child.
222
-
223
- The standard repository's `withTransaction(work)` delegates to
224
- `jsonRestApi.transaction(work)`. Its callback and mutation lifecycle phases
225
- receive the actual callable Knex transaction: custom repository SQL can use
226
- `await trx("audit_entries").insert(...)`, while resource calls receive it as
227
- `transaction`. Await every participant before returning. The helper owns
228
- commit, rollback, and completion hooks. Raw `knex.transaction()` handles cannot
229
- own resource writes, and calling `transaction()` again starts an independent
230
- unit; forward the existing handle to compose work.
190
+ | Before | Now |
191
+ | --- | --- |
192
+ | `defineCrudResource()` | `defineTableResource()` plus `defineCrudContract()` |
193
+ | `defineCrudJsonApiFeature()`, `createCrudResourceRuntime()` | `defineCrudFeature({ crud })` |
194
+ | the CRUD runtime's `listByIds()`, `listByForeignIds()` | `queryAll({ filters: { [key]: ids } })` on a filter declared `{ type: "array", filterOperator: "in" }`; a page's children with `createHooksToCollectChildren()` |
195
+ | the CRUD runtime's lookup hydration | `include` on the CRUD list or view |
196
+ | the CRUD runtime's `virtualFields` | `queryFields` in the `owns` entry, or `decorateService` for a value SQL cannot compute |
197
+ | the CRUD runtime's `createdAt`/`updatedAt` stamping | every handle write stamps them; only a system context sets its own |
198
+ | the CRUD runtime's output validation | the table resource's schema, whose types json-rest-api reads each field as |
199
+ | `defineCrudFeature`'s `ownershipFilter`, `searchSchema` | `visibility`; an `owns` entry |
200
+ | a lifecycle hook's `trx`; a decorator's or CRUD capability's `resource` | `options`; `crud` |
201
+ | `decorateRepository`'s `database`, `http`, `jsonRestApi` | its `repository` and `resources`, each owned table's handle |
202
+ | `@jskit-ai/crud-core/server/fieldAccess`, `/server/lookups` | removed (nothing called them); restrict fields in the CRUD contract and its `decorateService` |
203
+ | `runtime.json-rest-api`, `addResourceIfMissing()`, `createJsonRestResourceScopeOptions()` | `defineFeature({ owns })`; a row policy, search schema or query fields go in the `owns` entry |
204
+ | `queryDocuments()`, `getDocumentById()` | `query({ page: { size } })`, `get(id)` |
205
+ | `queryAllJsonRestCollectionRows()`, `queryEveryDocument()` | `queryAll(params)` |
206
+ | `createDocument()`, `createDocuments()` | `create()`, `createMany()` |
207
+ | `patchDocumentById()`, `patchDocumentIfLockVersion()` | `patch(id, changes, options)`, `patch(id, changes, { ...options, lockVersion })` |
208
+ | `deleteDocumentById()` | `delete(id)` |
209
+ | `getDocumentById(…, { forUpdate: true })`, `applyForUpdate()`, `lockScopedRecordId()` | `lock(id)` inside `withTransaction` |
210
+ | `runInTransaction()`, `createWithTransaction()`, `joinOrOpenTransaction()`, `resolveTransaction()`, `resolveQueryOptions()`, `resolveRepoClient()` | `withTransaction(options, (txOptions) => …)` |
211
+ | `returning: "full" \| "minimal"` | `returning: "record" \| "id"` (`"none"` is unchanged) |
212
+ | a hand-built `visibilityContext` or scope values, `applyVisibility()`, `applyVisibilityOwners()` | the caller's context, or `systemContext(reason).forWorkspace(id)` |
213
+ | a field's `default:` | `defaultTo:` (`default` is refused, since json-rest-api never read it) |
214
+ | a `createdAt` or `updatedAt` default of now | nothing: drop it, since every handle write stamps them |
215
+ | a CRUD contract that showed every column | a contract shows only its `fields`, so list every field the API showed |
216
+ | reading every row | `queryAll`, which refuses above 10,000 rows, so page with `query()` |
217
+ | an app's own wrappers around old calls | the handle method they wrapped, then delete the wrapper |
218
+
219
+ Handles answer records, `{ id, ...fields }`, with a belongs-to field as the
220
+ related id; the CRUD service alone answers JSON:API documents. Every call takes
221
+ `options` last: the caller's context, and inside a transaction the `txOptions`
222
+ `withTransaction` handed its work. A call without a context throws on a table
223
+ that is not public.
224
+
225
+ json-rest-api 3 changes what a failed write means: any failed write inside a
226
+ managed transaction fails the whole transaction. "Insert, catch the duplicate,
227
+ then read" inside a transaction no longer works; use
228
+ `createMany(rows, { ...options, onConflict: { keys, action: "ignore" } })` and then read,
229
+ or read first. A `get`, `patch`, `delete` or `lock` of a missing or hidden
230
+ record throws json-rest-api's not-found error (a 404); with `missing: "skip"`
231
+ it answers `null`, without failing the caller's transaction. Upsert rows carry no
232
+ `id`, never repeat their conflict keys, and name the ownership field among
233
+ those keys; only a table with no owners and no row policy may key the upsert on
234
+ the id alone, `onConflict: { keys: ["id"] }`, with an id on every row. `patchWhere` and `deleteWhere` need filters, or `all: true`.
231
235
 
232
236
  Preserve `error.transactionOutcome` and its cause when translating failures:
233
237
  `none` means no transaction was acquired or accepted, `pending` leaves
234
238
  completion to the owner, `rolledBack` confirms rollback, `committed` confirms
235
239
  commit, and `unknown` means the outcome is unresolved. These describe database
236
- transaction evidence, not external effects. A failed JSKIT mutation
237
- `afterCommit` hook raises `RestApiWriteError` with outcome `committed` and does
238
- not undo the write. Do not turn pending, committed, or unknown write failures
239
- into successful missing-record results, or automatically replay a committed
240
- or unresolved write.
240
+ transaction evidence, not external effects. A failed `afterCommit` hook raises
241
+ `RestApiWriteError` with outcome `committed` and does not undo the write. Do
242
+ not turn pending, committed, or unknown write failures into successful
243
+ missing-record results, or automatically replay a committed or unresolved
244
+ write.
245
+
246
+ ### Moving hand-written SQL behind resources
247
+
248
+ An application with Knex in its repositories moves it behind table resources
249
+ one table at a time:
250
+
251
+ 1. **Inventory.** List every statement that reads or writes a table: the
252
+ repository methods, raw queries, migrations' seed data, jobs and scripts.
253
+ `jskit check` and the data-access lint rules (`jskit/no-direct-knex`,
254
+ `jskit/no-hand-scope`) find most of them.
255
+ 2. **One owner per table.** Pick the feature that owns each table and give the
256
+ table one table resource with its `ownership`. Two features writing one
257
+ table means one of them asks the other.
258
+ 3. **Owner APIs.** Code outside the owner calls the owner's service or
259
+ repository capability. JSKIT's own tables already work this way: users
260
+ (`users.profiles`, `users.settings`), workspaces, local sign-in and console
261
+ settings.
262
+ 4. **Map each statement to a layer**
263
+ ([data access](/guide/framework/data-access)): one record or page →
264
+ Layer 1; screens over HTTP → Layer 2; a loop of writes → a many-row write;
265
+ a `forUpdate` read → a lock; totals and reports → `aggregate`, a window
266
+ include, or one `scopedQuery()` method; work over every workspace → a job
267
+ with `systemScope`.
268
+ 5. **Two tests per path.** `expectScoped` proves the path never reads or
269
+ changes a second owner's rows; `expectFlatQueries` proves its statements do
270
+ not grow with its rows. Both come from `@jskit-ai/resource-runtime/test`.
271
+
272
+ Keep each move behaviour-preserving
273
+ ([refactors](/guide/framework/sanity-checks#refactors)).
274
+
275
+ A table keyed by several columns, or by its owner's id, gets an integer `id`
276
+ first ([Layer 1](/guide/framework/data-access#layer-1-the-table-resource)).
277
+ One new migration does it on MySQL, MariaDB and PostgreSQL: the key becomes a
278
+ unique index before the primary key goes, so a foreign key on its first column
279
+ keeps an index, and adding the `id` numbers the existing rows. `down` undoes it
280
+ in reverse.
281
+
282
+ ```js
283
+ const KEY = ["workspace_id", "day"];
284
+
285
+ export async function up(knex) {
286
+ await knex.schema.alterTable("day_slots", (table) => table.unique(KEY, { indexName: "day_slots_key" }));
287
+ await knex.schema.alterTable("day_slots", (table) => table.dropPrimary());
288
+ await knex.schema.alterTable("day_slots", (table) => table.increments("id"));
289
+ }
290
+
291
+ export async function down(knex) {
292
+ await knex.schema.alterTable("day_slots", (table) => table.dropColumn("id"));
293
+ await knex.schema.alterTable("day_slots", (table) => table.primary(KEY));
294
+ await knex.schema.alterTable("day_slots", (table) => table.dropUnique(KEY, "day_slots_key"));
295
+ }
296
+ ```
297
+
298
+ MySQL and MariaDB commit each statement on its own, so a run that fails
299
+ part-way resumes as the [partial-failure
300
+ rules](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#directory-order-comes-before-filenames)
301
+ say. Code that wrote by the old key now upserts on it, with the ownership
302
+ column among the keys.
241
303
 
242
304
  ### Workspace invitation transaction compatibility (workspaces-core 0.1.198)
243
305
 
@@ -110,14 +110,25 @@ The package migrations create and own these tables:
110
110
  | `workspace_invites` | `workspace_id`, `email`, `role_sid`, `status`, `token_hash`, `invited_by_user_id`, `expires_at`, `accepted_at`, `revoked_at` |
111
111
 
112
112
  Application records that belong to a workspace use a non-null `workspace_id`
113
- foreign key to `workspaces.id`. Read and change these tables through the
114
- `workspaces.core` repositories and services, not directly.
113
+ foreign key to `workspaces.id`. Read and change these tables through their
114
+ owners' capabilities (`workspaces`, `workspaces.memberships` and
115
+ `workspaces.core`, below), not directly.
115
116
 
116
117
  ## Server composition
117
118
 
118
119
  Use `workspaces/workspace-server` for role vocabulary, creation policy,
119
120
  invitations, settings, and app-owned invitation email rendering.
120
121
 
122
+ Two server settings are required, and the server refuses to start without
123
+ them: whether new workspaces accept invitations, and how long an invitation
124
+ lasts.
125
+
126
+ ```js
127
+ // config/server.js
128
+ config.workspaceSettings = { defaults: { invitesEnabled: true } };
129
+ config.workspaceMembers = { defaults: { inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000 } };
130
+ ```
131
+
121
132
  Workspace scope must reach repository queries. Parsing a workspace slug or
122
133
  hiding an admin link is not authorization. Membership and role checks happen
123
134
  before an operation discloses records or record existence.
@@ -125,14 +136,12 @@ before an operation discloses records or record existence.
125
136
  ### Workspace-scoped features and routes
126
137
 
127
138
  For a standard CRUD resource owned by a workspace, give its table the
128
- `workspace_id` column, set the resource's `autofilter: "workspace"`, and bind
129
- `defineCrudJsonApiFeature()` with `ownershipFilter: "workspace"` and `scope:
130
- WORKSPACE_CRUD_SCOPE` (from `@jskit-ai/workspaces-core/server/support/workspaceCrud`)
131
- on a workspace surface. JSKIT then filters `workspaceId` by the workspace in the
132
- route. A repository method you add reads the workspace id with
133
- `resolveWorkspaceScopeValue(options.context)`
134
- (`@jskit-ai/json-rest-api-core/server/jsonRestApiHost`), which throws when the
135
- context names two workspaces.
139
+ `workspace_id` column, set its table resource's `ownership: "workspace"`, and bind
140
+ `defineCrudFeature()` with `scope: WORKSPACE_CRUD_SCOPE` (from
141
+ `@jskit-ai/workspaces-core/server/support/workspaceCrud`) on a workspace
142
+ surface. JSKIT then filters `workspaceId` by the workspace in the route. A
143
+ repository method you add passes the call's `options` on to the table's handle,
144
+ which scopes every read and write to the context's workspace.
136
145
  `workspace_user` also filters by the signed-in user. See the CRUD operations
137
146
  guide.
138
147
 
@@ -175,9 +184,29 @@ capability provides:
175
184
  | `workspaces.services.members` | Members, roles and invitations |
176
185
  | `workspaces.services.pendingInvitations` | Invitation resolution, redemption and acceptance participants |
177
186
  | `workspaces.services.settings` | Workspace settings |
178
- | `workspaces.repositories.workspaces`, `.workspaceMemberships`, `.workspaceSettings`, `.workspaceInvites` | Repository access inside the application's own transactions |
187
+ | `workspaces.repositories.workspaceSettings`, `.workspaceInvites` | Settings and invitation records inside the application's own transactions |
179
188
  | `workspaces.tenancyProfile` | The resolved tenancy mode and workspace policy |
180
189
 
190
+ The workspaces and their memberships are their own capabilities, for code that
191
+ reads or changes them inside its own transactions, typed `Workspaces` and
192
+ `WorkspacesMemberships` in `@jskit-ai/workspaces-core/server/types`. Every
193
+ method takes the call's `options` last and joins the transaction of the
194
+ `txOptions` it is given.
195
+
196
+ | Capability | Methods |
197
+ | --- | --- |
198
+ | `workspaces` | `findById(id, options)`, `findBySlug(slug, options)` (a deleted workspace is not found, though its slug stays taken), `listSlugsIn`, `findPersonalByOwnerUserId`, `listForUserId(userId, options)`, `insert`, `updateById`, `lock(workspaceId, options)` |
199
+ | `workspaces.memberships` | `findByWorkspaceIdAndUserId`, `ensureOwnerMembership`, `upsertMembership(workspaceId, userId, patch, options)`, `addMember({ workspaceId, userId, roleSid }, options)`, `activeMemberIds(workspaceId, options)`, `listActiveByWorkspaceId`, `findActiveByWorkspaceIdAndUserId`, `listActiveWorkspaceIdsByUserId`, `listActiveWorkspaceIdsByUserIds` |
200
+
201
+ `addMember` makes the user an active member with the role (`member` when none
202
+ is given), adding the membership or reactivating it. `activeMemberIds` answers
203
+ the active members' user ids at the same cost however many members there
204
+ are, without including their profiles. `lock` takes
205
+ the workspace's row lock until the caller's transaction ends, so work that must
206
+ not interleave for one workspace takes turns; it runs only inside a
207
+ transaction, given its `txOptions`, and answers `{ id }`, or `null` when there
208
+ is no such workspace.
209
+
181
210
  To check one person's membership, for example a notification recipient, call
182
211
  `workspaces.services.members.getMember(workspace, userId, options)`. It answers
183
212
  that member's `listMembers` entry, or `null` for a removed or unknown member,
@@ -225,8 +254,8 @@ workspace routes, `workspaces-web` switches Vuetify to the `workspace-light` or
225
254
  colours. Other routes use the application's themes.
226
255
 
227
256
  New workspaces start from JSKIT's blue palette unless `config/server.js` sets
228
- the application's own. Missing fields keep JSKIT's values; an invalid colour
229
- stops the server at startup:
257
+ the application's own beside the required `invitesEnabled`. Missing colours
258
+ keep JSKIT's values; an invalid colour stops the server at startup:
230
259
 
231
260
  ```js
232
261
  // config/server.js
@@ -301,11 +330,11 @@ authorization and role checks. Do not pass arbitrary HTTP input as options.
301
330
 
302
331
  | Capability method | Contract |
303
332
  | --- | --- |
304
- | `workspaces.repositories.workspaceInvites.withTransaction(work)` | Delegates to the same JSON REST API's managed `transaction(work)`; awaits commit and completion hooks before resolving. Runs `work` again when the database aborts it as a deadlock victim (see [Database layer](https://mobily-enterprises.github.io/jskit-ai/guide/app-setup/database-layer#deadlock-retries)). |
305
- | `workspaces.repositories.workspaceInvites.lockWorkspaceForInvitations(workspaceId, { trx })` | Locks the workspace before application receipt/draft rows when composing preparation. Requires an active transaction. |
306
- | `workspaces.services.members.prepareInvite(workspace, actor, payload, options)` | Requires a managed `options.trx`. Validates the invitation role, replaces pending invitations for that workspace/email, and returns `{ createdInviteId, inviteTokenPreview }`. Never sends. |
307
- | `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Requires no `options.trx`. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
308
- | `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Rejects `options.trx`. |
333
+ | `workspaces.repositories.workspaceInvites.withTransaction(options, (txOptions) => …)` | The invitations handle's `withTransaction`: joins the caller's transaction or opens one and hands `work` the `txOptions` to pass on; awaits commit and completion hooks before resolving. Runs `work` again when the database aborts it as a deadlock victim (see [data access](https://mobily-enterprises.github.io/jskit-ai/guide/framework/data-access#transactions)). `{ ...options, outermost: true }` refuses a call that already runs inside one. |
334
+ | `workspaces.lock(workspaceId, txOptions)` (capability `workspaces`) | Locks the workspace before application receipt/draft rows when composing preparation. Requires an active transaction. |
335
+ | `workspaces.services.members.prepareInvite(workspace, actor, payload, txOptions)` | Runs only inside a transaction, given its `txOptions`. Validates the invitation role, replaces pending invitations for that workspace/email, and returns `{ createdInviteId, inviteTokenPreview }`. Never sends. |
336
+ | `workspaces.services.members.sendInvite(workspace, actor, prepared, options)` | Refuses to run inside a transaction: it delivers after the preparation's transaction commits. Reloads the committed pending invitation and workspace display data, validates expiry and token correspondence, and sends that invitation without replacement. |
337
+ | `workspaces.services.members.createInvite(workspace, actor, payload, options)` | Owns preparation and commit, then calls `sendInvite`, and answers with `sendInvite`'s result as JSON:API data (`returnJsonApiData`, read it from `.value`), as the invite action sends it. Refuses to run inside a transaction. |
309
338
  | `workspaces.invite-acceptance-participant` (optional capability) | One application function receiving `({ invite, user }, options)` inside the acceptance transaction. Without it, acceptance activates the membership only. |
310
339
 
311
340
  `sendInvite` accepts `{ createdInviteId, inviteTokenPreview }` from preparation or
@@ -344,21 +373,20 @@ setup: ({ reviewedPeople, people, scopes, audit }) => ({
344
373
  })
345
374
 
346
375
  // Inside an authorized application command, in a feature that requires
347
- // workspaces: "workspaces.core":
376
+ // workspaces: "workspaces.core" and workspaceRecords: "workspaces":
348
377
  const { members } = workspaces.services;
349
378
  const invites = workspaces.repositories.workspaceInvites;
350
- const prepared = await invites.withTransaction(async (trx) => {
351
- const options = { trx, context };
379
+ const prepared = await invites.withTransaction({ context }, async (txOptions) => {
352
380
  // Match acceptance's order: workspace, then application draft/receipt rows.
353
- await invites.lockWorkspaceForInvitations(workspace.id, options);
354
- const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor, options);
355
- const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId, options);
381
+ await workspaceRecords.lock(workspace.id, txOptions);
382
+ const reviewed = await reviewedPeople.lockAuthorizedDraft(draftId, workspace.id, actor, txOptions);
383
+ const receipt = await reviewedPeople.findMatchingReceipt(reviewed, requestId, txOptions);
356
384
  if (receipt) return { createdInviteId: receipt.inviteId };
357
385
  const created = await members.prepareInvite(workspace, actor, {
358
386
  email: reviewed.email,
359
387
  roleSid: reviewed.roleSid
360
- }, options);
361
- await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId, options);
388
+ }, txOptions);
389
+ await reviewedPeople.associateInvitationAndSaveReceipt(reviewed, requestId, created.createdInviteId, txOptions);
362
390
  return { createdInviteId: created.createdInviteId };
363
391
  });
364
392
 
@@ -401,8 +429,8 @@ exactly once. Revocation after sending starts cannot recall a message.
401
429
  Register one callback during feature setup to call the required application
402
430
  services. Non-functions throw `TypeError`; duplicate registration throws, and
403
431
  registration closes when acceptance/refusal first begins. The callback's invite,
404
- user and options are shallow frozen. JSKIT owns the transaction and passes its
405
- `trx` and request `context` to the callback.
432
+ user and options are shallow frozen. JSKIT owns the transaction and passes the
433
+ callback the `txOptions` that carry it and the request's `context`.
406
434
 
407
435
  Acceptance checks authentication, recipient email, pending state and expiry under
408
436
  workspace/invitation locks. Before calling the participant, it activates membership
@@ -420,8 +448,8 @@ callback must not commit, roll back, create transactions/savepoints or perform
420
448
  external effects. Participant and later invitation-status failures roll back
421
449
  membership (including its previous role/status), person/training links, scopes,
422
450
  audit and accepted status. Invalid invitations and refusals do not call the
423
- participant. Acceptance/refusal reject borrowed `options.trx`; revocation can
424
- join a managed transaction or create one.
451
+ participant. Acceptance and refusal refuse to run inside the caller's
452
+ transaction; revocation can join one or open its own.
425
453
 
426
454
  Acceptance/refusal return 401 for unauthenticated access, 403 for a recipient
427
455
  email mismatch and 404 for a missing or handled invitation. A matching recipient's
@@ -435,7 +463,7 @@ Preserve `error.transactionOutcome` and `error.cause`: `rolledBack` confirms
435
463
  rollback, `committed` can accompany a failed completion hook, and `unknown`
436
464
  requires resolving the outcome before retrying. Errors after commit cannot undo
437
465
  persistence. See
438
- [managed transaction guidance](./existing-application-migration.md#json-rest-v2-integration).
466
+ [managed transaction guidance](./existing-application-migration.md#data-access-on-table-resources-json-rest-api-3).
439
467
 
440
468
  Invitation mutations lock workspace before invitation rows. A losing acceptance,
441
469
  refusal, revocation or replacement cannot overwrite accepted/revoked state.
@@ -494,7 +522,41 @@ Workspace surfaces put the slug in their pages root, such as
494
522
  `w/[workspaceSlug]/admin` for administration. Keep the default surface
495
523
  (`surfaceDefaultId`, usually `home`) free of a workspace requirement: `/`,
496
524
  invitation links and other unscoped pages belong to it. With
497
- `webRootAllowed: "no"`, `/` opens that surface's root.
525
+ `webRootAllowed: "no"`, `/` opens that surface's root. The foundation
526
+ defines `home` and its `public` access policy, and the `auth/auth-surface`
527
+ pattern the `auth` surface; the workspace surfaces add a policy and two
528
+ definitions:
529
+
530
+ ```js
531
+ // config/surfaceAccessPolicies.js
532
+ surfaceAccessPolicies.workspace_member = { requireAuth: true, requireWorkspaceMembership: true };
533
+
534
+ // config/public.js
535
+ config.surfaceDefinitions.app = {
536
+ id: "app",
537
+ label: "App",
538
+ pagesRoot: "w/[workspaceSlug]",
539
+ requiresAuth: true,
540
+ requiresWorkspace: true,
541
+ accessPolicyId: "workspace_member"
542
+ };
543
+ config.surfaceDefinitions.admin = {
544
+ id: "admin",
545
+ label: "Admin",
546
+ pagesRoot: "w/[workspaceSlug]/admin",
547
+ requiresAuth: true,
548
+ requiresWorkspace: true,
549
+ accessPolicyId: "workspace_member"
550
+ };
551
+ ```
552
+
553
+ An access policy takes `requireAuth`, `requireWorkspaceMembership`,
554
+ `requireAnyPermissions` and `requireAllPermissions` (permission lists), and
555
+ `requireFlagsAny` and `requireFlagsAll` (flags the server's bootstrap sets in
556
+ `surfaceAccess`). Without `requireAuth` or `requireWorkspaceMembership` it
557
+ takes the surface's `requiresAuth` or `requiresWorkspace`. The browser applies
558
+ the whole policy; the server reads only `requireWorkspaceMembership: false`
559
+ from it, and a route's own `auth` and `permission` authorize it there.
498
560
 
499
561
  `workspaces-web` remembers the last workspace each person opened in this
500
562
  browser. The **Your workspaces** page, `WorkspacesClientElement` from
@@ -58,7 +58,7 @@ questions and must not be copied into committed source.
58
58
  ## Build the first product operation
59
59
 
60
60
  Use the narrow relevant pattern and public APIs. For database CRUD, author an
61
- immutable migration and a normal shared resource contract; use framework APIs
61
+ immutable migration, its table resource and, for screens, a CRUD contract; use framework APIs
62
62
  for standard mechanics and app code for product-specific behavior. Do not
63
63
  invoke a source generator or reconstruct one as prompt options.
64
64
 
@@ -9,7 +9,7 @@ hub shows its sign-in page, the app receives an ID token, and the app keeps its
9
9
  own session.
10
10
 
11
11
  ```bash
12
- npm install @jskit-ai/oidc-issuer-core @jskit-ai/database-runtime-mysql
12
+ npm install @jskit-ai/oidc-issuer-core @jskit-ai/database-runtime @jskit-ai/database-runtime-mysql
13
13
  ```
14
14
 
15
15
  Start from the "JSKIT · Accounts hub" template (seed-jskit branch
@@ -62,7 +62,10 @@ npm run jskit:update
62
62
  ```
63
63
 
64
64
  The updater rejects unknown JSKIT packages and private JSKIT overrides during
65
- preflight, before changing any manifest.
65
+ preflight, before changing any manifest. It removes a package a release retired
66
+ (`@jskit-ai/json-rest-api-core`, replaced by `@jskit-ai/resource-runtime`) and
67
+ says what replaced it; while the application's code still imports a retired
68
+ package, it refuses and names the import to use instead.
66
69
 
67
70
  `jskit:update` reads the latest published catalog, discovers the root manifest
68
71
  and npm workspaces, and preflights every declared `@jskit-ai/*` package. It then
@@ -78,6 +81,15 @@ installation, or validation fails, it restores the previous manifests,
78
81
  lockfile, and installed tree before reporting the failure. Resolve the reported
79
82
  registry or real peer problem and rerun `npm run jskit:update`.
80
83
 
84
+ When the install brings package migrations the application has not had, the
85
+ updater lists each one (`<package>: <file>`) and says to run
86
+ `npm run db:migrate`. Run it, on every database the application uses, before
87
+ the new version serves requests: a package's new version reads what its
88
+ migration creates, so until then those reads fail, such as every local sign-in
89
+ with an unknown `email_verified_at` column. Read each package's CHANGELOG
90
+ **Upgrading** section too, for the changes the application itself must make,
91
+ such as a feature to register.
92
+
81
93
  ## Separately composed packages
82
94
 
83
95
  Pass each additional manifest explicitly to both commands:
@@ -210,8 +222,10 @@ application:
210
222
  `defineConfig(browserTestConfig({ testDir: "./tests/e2e", prepare: "tests/e2e/databases.js" }))`
211
223
  from `@jskit-ai/jskit-catalog/browser-tests`, with `server`, `build`, `env`
212
224
  or `port` only where its own differ from `node ./bin/server.js`,
213
- `npm run build` and 4173, and `globalTimeout` only for a suite measured to
214
- need more than 20 minutes. Settings a config adds (projects, a globalSetup
225
+ `npm run build` and 4173, `globalTimeout` only for a suite measured to
226
+ need more than 20 minutes, and `actionTimeout` or `navigationTimeout` only
227
+ for an action or page measured to need more than 15 or 30 seconds (a stale
228
+ locator should fail in seconds, not minutes). Settings a config adds (projects, a globalSetup
215
229
  that proves the server's identity, `timezoneId`) spread over the result:
216
230
  `defineConfig({ ...base, projects, use: { ...base.use, timezoneId } })`. A
217
231
  second local Playwright config and any `npm run develop` or `vite`
@@ -44,11 +44,14 @@ in-page banner that moves the form.
44
44
 
45
45
  Use `users/user-administration-server` only when the product needs global or
46
46
  workspace-scoped administration beyond self-service settings. It demonstrates
47
- an app-owned feature package built with `defineCrudJsonApiFeature()` while
48
- keeping profile persistence in `users-core`.
49
-
50
- Do not expose password hashes, provider credentials, or session data through a
51
- user resource. Apply permission and workspace scope before repository access so
47
+ an app-owned feature package built with `defineCrudFeature()` on a CRUD
48
+ contract over users-core's `userProfileTableResource`, while keeping profile
49
+ persistence, and the `users` table's one table resource, in `users-core`: the
50
+ feature names `ownerRepository: "users.profiles"` and serves the table through
51
+ users-core's repository instead of owning it.
52
+
53
+ Do not expose password hashes, provider credentials, or session data through
54
+ the CRUD contract. Apply permission and workspace scope before repository access so
52
55
  record existence is not leaked.
53
56
 
54
57
  ## Verification
@@ -101,7 +101,7 @@ normal order is:
101
101
 
102
102
  1. confirm the product resource, ownership, operations, and fields
103
103
  2. author a migration as normal application source
104
- 3. author the shared resource contract through `resource-crud-core`
104
+ 3. define the table resource (`defineTableResource`) and, for screens, its CRUD contract (`defineCrudContract`)
105
105
  4. use framework APIs for standard repository/service/action/route mechanics
106
106
  5. author product-specific screens from the relevant UI pattern
107
107
  6. run migrations and direct verification