@jskit-ai/agent-docs 0.1.1

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 (73) hide show
  1. package/DISTR_AGENT.md +25 -0
  2. package/guide/agent/app-extras/assistant.md +636 -0
  3. package/guide/agent/app-extras/realtime.md +223 -0
  4. package/guide/agent/app-setup/a-more-interesting-shell.md +643 -0
  5. package/guide/agent/app-setup/authentication.md +948 -0
  6. package/guide/agent/app-setup/console.md +316 -0
  7. package/guide/agent/app-setup/database-layer.md +775 -0
  8. package/guide/agent/app-setup/initial-scaffolding.md +714 -0
  9. package/guide/agent/app-setup/multi-homing.md +655 -0
  10. package/guide/agent/app-setup/users.md +355 -0
  11. package/guide/agent/app-setup/working-with-the-jskit-cli.md +983 -0
  12. package/guide/agent/generators/advanced-cruds.md +923 -0
  13. package/guide/agent/generators/crud-generators.md +556 -0
  14. package/guide/agent/generators/intro.md +63 -0
  15. package/guide/agent/generators/ui-generators.md +648 -0
  16. package/guide/agent/index.md +39 -0
  17. package/guide/human/app-extras/assistant.md +695 -0
  18. package/guide/human/app-extras/realtime.md +270 -0
  19. package/guide/human/app-setup/a-more-interesting-shell.md +734 -0
  20. package/guide/human/app-setup/authentication.md +963 -0
  21. package/guide/human/app-setup/console.md +352 -0
  22. package/guide/human/app-setup/database-layer.md +822 -0
  23. package/guide/human/app-setup/initial-scaffolding.md +738 -0
  24. package/guide/human/app-setup/multi-homing.md +795 -0
  25. package/guide/human/app-setup/users.md +404 -0
  26. package/guide/human/app-setup/working-with-the-jskit-cli.md +997 -0
  27. package/guide/human/generators/advanced-cruds.md +923 -0
  28. package/guide/human/generators/crud-generators.md +556 -0
  29. package/guide/human/generators/intro.md +109 -0
  30. package/guide/human/generators/ui-generators.md +665 -0
  31. package/guide/human/index.md +39 -0
  32. package/package.json +28 -0
  33. package/reference/autogen/KERNEL_MAP.md +536 -0
  34. package/reference/autogen/README.md +44 -0
  35. package/reference/autogen/packages/agent-docs.md +13 -0
  36. package/reference/autogen/packages/assistant-core.md +310 -0
  37. package/reference/autogen/packages/assistant-runtime.md +219 -0
  38. package/reference/autogen/packages/assistant.md +73 -0
  39. package/reference/autogen/packages/auth-core.md +352 -0
  40. package/reference/autogen/packages/auth-provider-supabase-core.md +223 -0
  41. package/reference/autogen/packages/auth-web.md +267 -0
  42. package/reference/autogen/packages/console-core.md +116 -0
  43. package/reference/autogen/packages/console-web.md +37 -0
  44. package/reference/autogen/packages/crud-core.md +283 -0
  45. package/reference/autogen/packages/crud-server-generator.md +220 -0
  46. package/reference/autogen/packages/crud-ui-generator.md +154 -0
  47. package/reference/autogen/packages/database-runtime-mysql.md +61 -0
  48. package/reference/autogen/packages/database-runtime-postgres.md +39 -0
  49. package/reference/autogen/packages/database-runtime.md +216 -0
  50. package/reference/autogen/packages/http-runtime.md +213 -0
  51. package/reference/autogen/packages/kernel.md +1350 -0
  52. package/reference/autogen/packages/realtime.md +95 -0
  53. package/reference/autogen/packages/shell-web.md +349 -0
  54. package/reference/autogen/packages/storage-runtime.md +39 -0
  55. package/reference/autogen/packages/ui-generator.md +101 -0
  56. package/reference/autogen/packages/uploads-image-web.md +76 -0
  57. package/reference/autogen/packages/uploads-runtime.md +85 -0
  58. package/reference/autogen/packages/users-core.md +307 -0
  59. package/reference/autogen/packages/users-web.md +473 -0
  60. package/reference/autogen/packages/workspaces-core.md +415 -0
  61. package/reference/autogen/packages/workspaces-web.md +372 -0
  62. package/reference/autogen/tooling/config-eslint.md +52 -0
  63. package/reference/autogen/tooling/create-app.md +194 -0
  64. package/reference/autogen/tooling/jskit-catalog.md +27 -0
  65. package/reference/autogen/tooling/jskit-cli.md +624 -0
  66. package/reference/autogen/tooling/test-support.md +27 -0
  67. package/reference/autogen/tooling/testUtils.md +31 -0
  68. package/templates/APP_BLUEPRINT.md +57 -0
  69. package/workflow/app-state.md +33 -0
  70. package/workflow/bootstrap.md +24 -0
  71. package/workflow/feature-delivery.md +21 -0
  72. package/workflow/review.md +22 -0
  73. package/workflow/scoping.md +26 -0
@@ -0,0 +1,795 @@
1
+ <!-- Generated by `npm run agent-docs:build` from `docs/guide/app-setup/multi-homing.md`. Do not edit manually. -->
2
+
3
+ # Multi-homing
4
+
5
+ Up to this point, the app has had several surfaces, but none of them were workspace-dependent. `home`, `auth`, `account`, and `console` all live outside any workspace slug.
6
+
7
+ This chapter changes that. We turn the app into a workspace-aware application and install the packages that add the first real workspace surfaces.
8
+
9
+ If you are rebuilding this chapter from scratch, the cleanest setup is to start with a workspace-capable tenancy mode from day 0. If you are literally continuing from the previous chapter, you can still move the existing app from `none` to `personal`, but that retrofit has to be done in the right order.
10
+
11
+ ## Tenancy modes
12
+
13
+ JSKIT currently accepts three tenancy modes:
14
+
15
+ - `none`
16
+ - no workspace routing
17
+ - no `/w/[workspaceSlug]` surfaces
18
+ - useful for a purely global app with no workspace concept
19
+ - `personal`
20
+ - workspace routing is enabled
21
+ - each user gets one auto-provisioned personal workspace
22
+ - the workspace slug is derived from the user's identity and treated as immutable
23
+ - creating additional workspaces is off by default
24
+ - `workspaces`
25
+ - workspace routing is enabled
26
+ - users can belong to multiple named workspaces
27
+ - workspace slugs are user-selected rather than derived from the username
28
+ - auto-provisioning is off by default, and self-creation is a separate policy choice
29
+
30
+ Both `personal` and `workspaces` are workspace-capable modes, so they allow the workspace package descriptors to install the full workspace scaffold.
31
+
32
+ This chapter teaches `personal`, not `workspaces`.
33
+
34
+ That is deliberate. `personal` gives the guide a much better first-run experience because the first workspace is auto-provisioned for the user. The app still becomes multi-homing-capable, because invitations and memberships can still put one user in several workspaces at once. The `personal` part only changes how the first workspace is provisioned and how its slug policy works.
35
+
36
+ ## Recap from previous chapters
37
+
38
+ To recreate the previous chapter's package set in a fresh app that is already ready for workspace routing, run:
39
+
40
+ ```bash
41
+ SUPABASE_URL=...
42
+ SUPABASE_KEY=...
43
+ DB_HOST=127.0.0.1
44
+ DB_PORT=3306
45
+ DB_NAME=exampleapp
46
+ DB_USER=exampleapp
47
+ DB_PASSWORD=secret
48
+
49
+ npx @jskit-ai/create-app exampleapp --tenancy-mode personal
50
+ cd exampleapp
51
+ npm install
52
+
53
+ npx jskit add package shell-web
54
+ npx jskit add package auth-provider-supabase-core \
55
+ --auth-supabase-url "$SUPABASE_URL" \
56
+ --auth-supabase-publishable-key "$SUPABASE_KEY" \
57
+ --app-public-url "http://localhost:5173"
58
+ npx jskit add bundle auth-base
59
+ npx jskit add package database-runtime-mysql \
60
+ --db-host "$DB_HOST" \
61
+ --db-port "$DB_PORT" \
62
+ --db-name "$DB_NAME" \
63
+ --db-user "$DB_USER" \
64
+ --db-password "$DB_PASSWORD"
65
+ npx jskit add package users-web
66
+ npx jskit add package console-web
67
+ npm install
68
+ npm run db:migrate
69
+ ```
70
+
71
+ If you are already continuing from the previous chapter, you do not need to rebuild the app from scratch. Read the textbox below first, then continue with the workspace package install.
72
+
73
+ <DocsTerminalTip label="Important" title="Moving An Existing `none` App To `personal`">
74
+ If you followed the guide in order, your existing app still has:
75
+
76
+ ```js
77
+ config.tenancyMode = "none";
78
+ ```
79
+
80
+ To move that app to workspace routing safely, do this **before** installing `workspaces-core` or `workspaces-web`:
81
+
82
+ 1. Edit `config/public.js` and change:
83
+
84
+ ```js
85
+ config.tenancyMode = "personal";
86
+ ```
87
+
88
+ 2. Add `workspaces-core`
89
+ 3. Run `npm install`
90
+ 4. Add `workspaces-web`
91
+ 5. Run `npm install`
92
+ 6. Run `npm run db:migrate`
93
+
94
+ I checked the two failure modes while updating this chapter.
95
+
96
+ - If the app is already in `personal` or `workspaces` mode when you add the workspace packages, JSKIT installs the full workspace scaffold, including:
97
+ - `src/pages/w/[workspaceSlug]...`
98
+ - `src/components/WorkspaceNotFoundCard.vue`
99
+ - `src/composables/useWorkspaceNotFoundState.js`
100
+ - the workspace placement entries in `src/placement.js`
101
+ - If you add `workspaces-core` and `workspaces-web` while the app is still on `none`, JSKIT installs only the non-gated pieces. The app can still build, but those workspace route files and placements are missing.
102
+
103
+ That last part is the trap: simply changing `config.tenancyMode` afterwards and running `npx jskit add package workspaces-core` or `workspaces-web` again does **not** backfill the skipped scaffold.
104
+
105
+ If you already installed either workspace package while the app was still on `none`, the clean recovery path is:
106
+
107
+ 1. Change `config.tenancyMode` to `"personal"`
108
+ 2. Run `npx jskit update package workspaces-core`
109
+ 3. Run `npx jskit update package workspaces-web`
110
+ 4. Run `npm install`
111
+ 5. Run `npm run db:migrate`
112
+
113
+ When I tested that recovery path, JSKIT warned that some workspace migrations already existed and skipped re-installing them. That is expected. The important part is that `update package ...` re-applies the gated workspace scaffold after the tenancy mode has been fixed.
114
+ </DocsTerminalTip>
115
+
116
+ ## Installing the workspace packages
117
+
118
+ If your app is already on `tenancyMode = "personal"`, run:
119
+
120
+ ```bash
121
+ npx jskit add package workspaces-core
122
+ npm install
123
+ npx jskit add package workspaces-web
124
+ npm install
125
+ npm run db:migrate
126
+ ```
127
+
128
+ `workspaces-core` adds the server-side workspace runtime and schema migrations. `workspaces-web` adds the workspace-facing client surfaces, shell placements, and app-owned route files.
129
+
130
+ If you want to inspect that package before installing it, this is a very good moment to use the CLI chapter's inspection command:
131
+
132
+ ```bash
133
+ npx jskit show @jskit-ai/workspaces-web --details
134
+ ```
135
+
136
+ That output makes the package feel much less mysterious, because it shows the exact workspace shell contributions, settings outlets, client tokens, app-owned file writes, and capability requirements before you mutate the app.
137
+
138
+ ## What changes now
139
+
140
+ This chapter is where the app stops being a collection of global surfaces and starts supporting workspace-dependent ones.
141
+
142
+ ### Two new workspace surfaces appear
143
+
144
+ After the install, the app gains:
145
+
146
+ - an `app` surface rooted at `/w/[workspaceSlug]`
147
+ - an `admin` surface rooted at `/w/[workspaceSlug]/admin`
148
+
149
+ These are real surfaces, not only pages.
150
+
151
+ - `app` is the generic workspace surface that normal authenticated workspace members can use
152
+ - `admin` is the richer workspace administration surface
153
+
154
+ That distinction matters.
155
+
156
+ The `app` surface is the "normal" workspace area. It is where you would usually put the main member-facing experience for that workspace: dashboards, documents, tasks, project views, customer-facing content, or whatever the ordinary in-workspace product actually is.
157
+
158
+ The `admin` surface is where the workspace is managed. It is the natural place for things like:
159
+
160
+ - inviting other users into the workspace
161
+ - managing members and roles
162
+ - editing workspace settings
163
+ - running workspace-specific admin tools
164
+
165
+ So `admin` is not just "another page." It is the surface where the workspace itself is configured and operated.
166
+
167
+ There is also no rule that says every app must use these two surfaces in the same way.
168
+
169
+ - In one app, `app` could be the real product and `admin` could be the backend for managing it.
170
+ - In another app, `admin` might be where most of the real work happens, while `app` stays minimal or even almost empty.
171
+ - In a storefront-style app, `app` could be the workspace's visible shop area and `admin` could be the merchant backend.
172
+
173
+ The important point is not the label. The important point is the split:
174
+
175
+ - `app` is the general workspace-facing surface
176
+ - `admin` is the management and control surface for that workspace
177
+
178
+ That is why this chapter feels larger than the previous ones. It is not just adding another route. It is adding a new routing topology.
179
+
180
+ ### The shell and account surface gain workspace-aware controls
181
+
182
+ The placement registry now grows a workspace selector in the top-left of the shell, a pending-invites cue in the top-right area, and workspace tools in the admin shell. The workspaces package also plugs an `Invites` section into the existing `/account` settings screen through the account-settings extension seam that `users-web` exposes.
183
+
184
+ That means the shell itself starts adapting to workspace context.
185
+
186
+ - on any authenticated surface, the shell can now expose a workspace selector
187
+ - signed-in users can now see a pending-invites cue without `users-web` owning that workspace feature
188
+ - on admin workspace surfaces, the shell can also expose workspace-specific tools and settings
189
+
190
+ This is the first time the guide shows the shell reacting not just to authentication state, but to workspace state as well.
191
+
192
+ ### The database schema grows real multi-workspace tables
193
+
194
+ `workspaces-core` adds the schema needed for:
195
+
196
+ - workspaces
197
+ - workspace memberships
198
+ - workspace settings
199
+ - workspace invites
200
+
201
+ That is why `npm run db:migrate` is required again in this chapter. The workspace runtime is not only client-side routing. It is persistent tenancy data.
202
+
203
+ ### Existing surfaces do not disappear
204
+
205
+ This is also important to notice:
206
+
207
+ - `home` still exists
208
+ - `auth` still exists
209
+ - `account` still exists
210
+ - `console` still exists
211
+
212
+ The new workspace surfaces are added on top of the existing app, not instead of it.
213
+
214
+ That is exactly what multi-homing should feel like. The app now has both:
215
+
216
+ - global surfaces
217
+ - workspace-scoped surfaces
218
+
219
+ ## What to look at in the browser
220
+
221
+ Start both processes again:
222
+
223
+ ```bash
224
+ npm run dev
225
+ npm run server
226
+ ```
227
+
228
+ After you sign in, the app now has the routing structure needed for workspace-aware paths such as:
229
+
230
+ ```text
231
+ /w/your-personal-slug
232
+ /w/your-personal-slug/admin
233
+ ```
234
+
235
+ In `personal` mode, the first workspace is auto-provisioned for the signed-in user, so you should have a real workspace route to open immediately after login. Later, if the same user is invited into another workspace, routes such as `/w/acme` can exist alongside that personal workspace too.
236
+
237
+ At this stage of the guide, the starter workspace pages are still intentionally simple. That is helpful. It lets you see the new routing and shell topology clearly before later chapters add real modules inside those surfaces.
238
+
239
+ The most important new visible ideas are:
240
+
241
+ - a workspace slug now appears in the route
242
+ - the shell can expose workspace selection and workspace tools
243
+ - workspace-dependent surfaces can show a dedicated unavailable-state card when the requested workspace cannot be resolved
244
+
245
+ First, even the old global `/home` surface now reacts to workspace state. The selector in the top-left and the invites cue in the top-right come from placement entries and workspace bootstrap context, not from the `home` page itself.
246
+
247
+ <figure class="docs-browser-shot">
248
+ <div class="docs-browser-shot__bar">
249
+ <div class="docs-browser-shot__dots" aria-hidden="true">
250
+ <span></span>
251
+ <span></span>
252
+ <span></span>
253
+ </div>
254
+ <div class="docs-browser-shot__address">http://localhost:5173/home</div>
255
+ </div>
256
+ <img
257
+ src="/images/guide/multi-homing/multi-homing-shell-controls.png"
258
+ alt="Example app home surface after the multi-homing chapter, showing the workspace selector and invites cue in the shell"
259
+ />
260
+ </figure>
261
+
262
+ Then open your personal workspace route. The page itself is deliberately light. `src/pages/w/[workspaceSlug]/index.vue` mostly exists to prove that the `app` surface is now real and ready to host later modules.
263
+
264
+ <figure class="docs-browser-shot">
265
+ <div class="docs-browser-shot__bar">
266
+ <div class="docs-browser-shot__dots" aria-hidden="true">
267
+ <span></span>
268
+ <span></span>
269
+ <span></span>
270
+ </div>
271
+ <div class="docs-browser-shot__address">http://localhost:5173/w/chiaramobily</div>
272
+ </div>
273
+ <img
274
+ src="/images/guide/multi-homing/multi-homing-workspace-home.png"
275
+ alt="Example app workspace home route after the multi-homing chapter"
276
+ />
277
+ </figure>
278
+
279
+ Next open the admin surface for the same workspace. This is the matching pattern on the admin side: `src/pages/w/[workspaceSlug]/admin.vue` is the shell wrapper, `src/pages/w/[workspaceSlug]/admin/index.vue` is the starter page, and the top-right workspace tools button is coming from placements rather than being hand-built into that page file.
280
+
281
+ <figure class="docs-browser-shot">
282
+ <div class="docs-browser-shot__bar">
283
+ <div class="docs-browser-shot__dots" aria-hidden="true">
284
+ <span></span>
285
+ <span></span>
286
+ <span></span>
287
+ </div>
288
+ <div class="docs-browser-shot__address">http://localhost:5173/w/chiaramobily/admin</div>
289
+ </div>
290
+ <img
291
+ src="/images/guide/multi-homing/multi-homing-workspace-admin.png"
292
+ alt="Example app workspace admin route after the multi-homing chapter"
293
+ />
294
+ </figure>
295
+
296
+ Finally open the nested workspace settings route. This is useful to inspect because it shows the container pattern most clearly. `src/pages/w/[workspaceSlug]/admin/workspace/settings.vue` owns the section frame and the child outlet, while the actual settings sub-pages can keep arriving later under that shell.
297
+
298
+ <figure class="docs-browser-shot">
299
+ <div class="docs-browser-shot__bar">
300
+ <div class="docs-browser-shot__dots" aria-hidden="true">
301
+ <span></span>
302
+ <span></span>
303
+ <span></span>
304
+ </div>
305
+ <div class="docs-browser-shot__address">http://localhost:5173/w/chiaramobily/admin/workspace/settings</div>
306
+ </div>
307
+ <img
308
+ src="/images/guide/multi-homing/multi-homing-workspace-settings.png"
309
+ alt="Example app workspace settings shell after the multi-homing chapter"
310
+ />
311
+ </figure>
312
+
313
+ ## What the workspace packages add to the app
314
+
315
+ This chapter changes the app in four main places:
316
+
317
+ - public config
318
+ - surface access policies
319
+ - migrations
320
+ - workspace surface route files and placements
321
+
322
+ ### `config/public.js` changes in a big way
323
+
324
+ The first change is the explicit tenancy mode:
325
+
326
+ ```js
327
+ config.tenancyMode = "personal";
328
+ ```
329
+
330
+ Then the app gets two new surface definitions:
331
+
332
+ ```js
333
+ config.surfaceDefinitions.app = {
334
+ id: "app",
335
+ label: "App",
336
+ pagesRoot: "w/[workspaceSlug]",
337
+ enabled: true,
338
+ requiresAuth: true,
339
+ requiresWorkspace: true,
340
+ accessPolicyId: "workspace_member",
341
+ origin: ""
342
+ };
343
+
344
+ config.surfaceDefinitions.admin = {
345
+ id: "admin",
346
+ label: "Admin",
347
+ pagesRoot: "w/[workspaceSlug]/admin",
348
+ enabled: true,
349
+ requiresAuth: true,
350
+ requiresWorkspace: true,
351
+ accessPolicyId: "workspace_member",
352
+ origin: ""
353
+ };
354
+ ```
355
+
356
+ And the app also gains workspace-level feature config:
357
+
358
+ ```js
359
+ config.workspaceSwitching = true;
360
+ config.workspaceInvitations = {
361
+ enabled: true,
362
+ allowInPersonalMode: true
363
+ };
364
+ ```
365
+
366
+ Those lines are the public contract that tells both client and server, "this app is workspace-aware now."
367
+
368
+ ### `config/surfaceAccessPolicies.js` gains `workspace_member`
369
+
370
+ The new workspace surfaces use a workspace membership rule:
371
+
372
+ ```js
373
+ surfaceAccessPolicies.workspace_member = {
374
+ requireAuth: true,
375
+ requireWorkspaceMembership: true
376
+ };
377
+ ```
378
+
379
+ This is the first time the guide shows a surface guarded not just by auth or a simple flag, but by real workspace membership.
380
+
381
+ That is the core idea of multi-homing in JSKIT:
382
+
383
+ - the route contains a workspace slug
384
+ - the server resolves that workspace
385
+ - access depends on whether the current user belongs to it
386
+
387
+ ### The migrations now include workspace schema
388
+
389
+ After `workspaces-core`, the migration directory grows again with files such as:
390
+
391
+ ```text
392
+ migrations/
393
+ 2026..._workspaces-core-initial-schema.cjs
394
+ 2026..._users-core-workspace-settings-single-name-source.cjs
395
+ 2026..._users-core-workspaces-drop-color.cjs
396
+ ```
397
+
398
+ These are the tables and schema changes that make workspace tenancy real in the database.
399
+
400
+ That is why the chapter needs another `npm run db:migrate`. Without those tables, the workspace runtime would have routes and UI, but nowhere to persist workspace membership and settings.
401
+
402
+ ### The route tree gains workspace-dependent pages
403
+
404
+ The app now gets:
405
+
406
+ ```text
407
+ src/pages/w/[workspaceSlug].vue
408
+ src/pages/w/[workspaceSlug]/index.vue
409
+ src/pages/w/[workspaceSlug]/admin.vue
410
+ src/pages/w/[workspaceSlug]/admin/index.vue
411
+ src/pages/w/[workspaceSlug]/admin/members/index.vue
412
+ src/pages/w/[workspaceSlug]/admin/workspace/settings.vue
413
+ src/pages/w/[workspaceSlug]/admin/workspace/settings/index.vue
414
+ ```
415
+
416
+ That list shows the first real nested workspace topology.
417
+
418
+ - `w/[workspaceSlug].vue` is the `app` surface wrapper
419
+ - `w/[workspaceSlug]/index.vue` is the starter landing page for the `app` surface
420
+ - `w/[workspaceSlug]/admin.vue` is the `admin` surface wrapper
421
+ - `w/[workspaceSlug]/admin/index.vue` is the starter landing page for the `admin` surface
422
+ - `w/[workspaceSlug]/admin/members/index.vue` mounts the first real workspace admin client element
423
+ - `w/[workspaceSlug]/admin/workspace/settings.vue` is a local section shell for nested workspace settings routes
424
+ - `w/[workspaceSlug]/admin/workspace/settings/index.vue` is the default child route for that settings shell
425
+
426
+ So the workspace surface model is not only a config concept. It becomes a real file-based routing tree in `src/pages/`.
427
+
428
+ These files are intentionally thinner than they look.
429
+
430
+ Most of the machinery happens up-hill from them:
431
+
432
+ - `config/public.js` defines which surfaces exist and which path roots they own
433
+ - `config/surfaceAccessPolicies.js` defines the membership rule that guards them
434
+ - `src/placement.js` wires the selector, invites cue, and admin tools into the shell
435
+ - `workspaces-core` and `workspaces-web` provide the bootstrap, workspace resolution, permissions, settings, and reusable client elements underneath those routes
436
+
437
+ So the `src/pages` files are mostly containers and composition points, not the place where workspace tenancy is implemented.
438
+
439
+ Concretely, that route tree works like this:
440
+
441
+ - `w/[workspaceSlug].vue` and `w/[workspaceSlug]/admin.vue` are almost pure wrappers. They tag the route with the correct surface id and mount `ShellLayout` plus a child `<RouterView />`.
442
+ - `w/[workspaceSlug]/index.vue` and `w/[workspaceSlug]/admin/index.vue` are intentionally simple starter cards. They prove that the new workspace surfaces are live, but they are meant to be replaced by real product modules later.
443
+ - `w/[workspaceSlug]/admin/members/index.vue` is still thin, but in a different way: it mostly hands control to a packaged `WorkspaceMembersClientElement`, so the route file stays small while the reusable member-management behavior lives in `workspaces-web`.
444
+ - `w/[workspaceSlug]/admin/workspace/settings.vue` is a section shell. It does not own the actual settings fields. It owns the card frame, the left-side settings menu outlet, and the nested `<RouterView />` where child settings pages render.
445
+ - `w/[workspaceSlug]/admin/workspace/settings/index.vue` is intentionally almost empty. Its job is to make `/admin/workspace/settings` a real route today and give you a clean place to redirect or add child settings pages later.
446
+
447
+ ### Workspace pages are prepared for missing-workspace states
448
+
449
+ The starter workspace pages already use a dedicated unavailable-state helper:
450
+
451
+ ```vue
452
+ <WorkspaceNotFoundCard
453
+ v-if="workspaceUnavailable"
454
+ :message="workspaceUnavailableMessage"
455
+ surface-label="App"
456
+ />
457
+ ```
458
+
459
+ That is worth noticing because it shows that workspace routing is not just string matching on `[workspaceSlug]`. The runtime is expected to decide whether the requested workspace is actually valid and accessible.
460
+
461
+ This is another example of the "mostly containers" pattern. The page file does not resolve the workspace itself. It asks the shared `useWorkspaceNotFoundState()` helper for the current workspace-bootstrap status and then swaps between:
462
+
463
+ - a shared unavailable card when the workspace is missing or inaccessible
464
+ - the local starter content when the workspace context is valid
465
+
466
+ So the starter pages already distinguish:
467
+
468
+ - valid workspace context
469
+ - invalid or inaccessible workspace context
470
+
471
+ ### The public workspace client API for custom pages
472
+
473
+ At this point in the chapter, it is worth separating two different kinds of client-side helpers:
474
+
475
+ - app-owned scaffold helpers written into `src/`
476
+ - public package helpers exported by `@jskit-ai/workspaces-web`
477
+
478
+ `useWorkspaceNotFoundState()` belongs to the first group. It is an app-owned helper scaffolded into:
479
+
480
+ ```text
481
+ src/composables/useWorkspaceNotFoundState.js
482
+ ```
483
+
484
+ That makes it easy to customize locally.
485
+
486
+ The main app-author-facing helper exported by `workspaces-web` itself is:
487
+
488
+ ```js
489
+ import { useWorkspaceRouteContext } from "@jskit-ai/workspaces-web/client/composables/useWorkspaceRouteContext";
490
+ ```
491
+
492
+ That is the public composable most custom workspace pages should reach for first.
493
+
494
+ #### What `workspaces-web` actually exposes publicly on the client side
495
+
496
+ Today, the public client surface is intentionally small.
497
+
498
+ - `@jskit-ai/workspaces-web/client`
499
+ - the package's client runtime registration surface
500
+ - exports `clientProviders`, `WorkspacesWebClientProvider`, and `WorkspaceMembersClientElement`
501
+ - `@jskit-ai/workspaces-web/client/composables/useWorkspaceRouteContext`
502
+ - the main public page-level composable for workspace-aware route context
503
+
504
+ That is a useful distinction.
505
+
506
+ - `WorkspacesWebClientProvider` and `clientProviders` are runtime wiring, not something a normal page imports.
507
+ - `WorkspaceMembersClientElement` is a packaged feature element that a route can render directly.
508
+ - `useWorkspaceRouteContext()` is the public helper most app-authored workspace pages will actually use.
509
+
510
+ There are other helpers inside the package source, but if they are not exported by the package, treat them as internal implementation details rather than app code API.
511
+
512
+ #### What `useWorkspaceRouteContext()` gives you
513
+
514
+ The composable returns:
515
+
516
+ - `route`
517
+ - the live Vue Router route object
518
+ - `routePath`
519
+ - the normalized runtime pathname
520
+ - `currentSurfaceId`
521
+ - the resolved current surface id, such as `app` or `admin`
522
+ - `workspaceSlugFromRoute`
523
+ - the current workspace slug, but only when the current route really belongs to a workspace-dependent surface
524
+ - `placementContext`
525
+ - the current shell placement/bootstrap context
526
+ - `mergePlacementContext`
527
+ - the function used to merge new context back into the shell runtime
528
+
529
+ For most custom pages, the two values you care about most are:
530
+
531
+ - `workspaceSlugFromRoute`
532
+ - `currentSurfaceId`
533
+
534
+ #### Why use it instead of reading `$route.params.workspaceSlug` directly
535
+
536
+ For a very simple page, reading the raw route param can work.
537
+
538
+ But `useWorkspaceRouteContext()` is the better default for workspace-aware app code because it does more than "read a param":
539
+
540
+ - it resolves the current surface through the shell placement/runtime context
541
+ - it normalizes the current route path before extracting anything
542
+ - it only returns a workspace slug when the current surface is actually workspace-scoped
543
+ - it works the same way on both workspace surfaces, `app` and `admin`
544
+
545
+ That means your page logic stays aligned with the same route model that the packaged workspace components use internally.
546
+
547
+ In other words:
548
+
549
+ - `$route.params.workspaceSlug` is a raw route detail
550
+ - `useWorkspaceRouteContext()` is a workspace-aware view of the current route
551
+
552
+ #### A concrete custom page example
553
+
554
+ Suppose you create a real admin page at:
555
+
556
+ ```text
557
+ src/pages/w/[workspaceSlug]/admin/reports/index.vue
558
+ ```
559
+
560
+ and that page needs to:
561
+
562
+ - know which workspace it is looking at
563
+ - know that it is running on the `admin` surface
564
+ - build a workspace-scoped API request or query key
565
+
566
+ That is exactly the kind of page `useWorkspaceRouteContext()` is for:
567
+
568
+ ```vue
569
+ <script setup>
570
+ import { computed } from "vue";
571
+ import { usePaths } from "@jskit-ai/users-web/client/composables/usePaths";
572
+ import { useWorkspaceRouteContext } from "@jskit-ai/workspaces-web/client/composables/useWorkspaceRouteContext";
573
+
574
+ const { workspaceSlugFromRoute, currentSurfaceId } = useWorkspaceRouteContext();
575
+ const paths = usePaths();
576
+
577
+ const reportsApiPath = computed(() => {
578
+ if (!workspaceSlugFromRoute.value) {
579
+ return "";
580
+ }
581
+
582
+ return paths.api("/reports", {
583
+ params: {
584
+ workspaceSlug: workspaceSlugFromRoute.value
585
+ }
586
+ });
587
+ });
588
+
589
+ const queryKey = computed(() => ([
590
+ "workspace-reports",
591
+ currentSurfaceId.value,
592
+ workspaceSlugFromRoute.value
593
+ ]));
594
+ </script>
595
+ ```
596
+
597
+ That pattern is intentionally boring in the right way.
598
+
599
+ - the page does not hard-code path parsing rules itself
600
+ - the page gets a normalized workspace slug from the public workspace helper
601
+ - the query key can still distinguish between `app` and `admin` when needed
602
+
603
+ The same composable works just as well in a custom workspace app page under:
604
+
605
+ ```text
606
+ src/pages/w/[workspaceSlug]/index.vue
607
+ ```
608
+
609
+ or any later child page below that surface.
610
+
611
+ #### When you need the rest of the returned context
612
+
613
+ Most custom pages only need:
614
+
615
+ - `workspaceSlugFromRoute`
616
+ - `currentSurfaceId`
617
+ - maybe `route`
618
+
619
+ The other returned values are there for more advanced cases.
620
+
621
+ - `placementContext` matters when a page needs to read the current shell/bootstrap context directly, for example available workspaces or current permissions already in the shell state.
622
+ - `mergePlacementContext` is for pages that need to push refreshed workspace data back into the shell runtime after a fetch or save.
623
+
624
+ That second case is real, but it is more advanced. It is what packaged elements such as the workspace settings client use when they need the shell's current workspace badge, selector state, or permissions to refresh after a change.
625
+
626
+ For normal custom page code, you usually do **not** start there. Start with `workspaceSlugFromRoute`.
627
+
628
+ ### `src/placement.js` becomes workspace-aware
629
+
630
+ The workspace packages append a new block of placements:
631
+
632
+ ```js
633
+ addPlacement({
634
+ id: "workspaces.profile.menu.surface-switch",
635
+ target: "auth-profile-menu:primary-menu",
636
+ surfaces: ["*"],
637
+ order: 100,
638
+ componentToken: "workspaces.web.profile.menu.surface-switch-item",
639
+ when: ({ auth }) => Boolean(auth?.authenticated)
640
+ });
641
+
642
+ addPlacement({
643
+ id: "workspaces.workspace.selector",
644
+ target: "shell-layout:top-left",
645
+ surfaces: ["*"],
646
+ order: 200,
647
+ componentToken: "workspaces.web.workspace.selector",
648
+ props: {
649
+ allowOnNonWorkspaceSurface: true,
650
+ targetSurfaceId: "app"
651
+ },
652
+ when: ({ auth }) => {
653
+ return Boolean(auth?.authenticated);
654
+ }
655
+ });
656
+
657
+ addPlacement({
658
+ id: "workspaces.account.invites.cue",
659
+ target: "shell-layout:top-right",
660
+ surfaces: ["*"],
661
+ order: 850,
662
+ componentToken: "local.main.account.pending-invites.cue",
663
+ when: ({ auth }) => Boolean(auth?.authenticated)
664
+ });
665
+
666
+ addPlacement({
667
+ id: "workspaces.workspace.tools.widget",
668
+ target: "shell-layout:top-right",
669
+ surfaces: ["admin"],
670
+ order: 900,
671
+ componentToken: "workspaces.web.workspace.tools.widget"
672
+ });
673
+
674
+ addPlacement({
675
+ id: "workspaces.workspace.menu.workspace-settings",
676
+ target: "workspace-tools:primary-menu",
677
+ surfaces: ["admin"],
678
+ order: 100,
679
+ componentToken: "workspaces.web.workspace-settings.menu-item"
680
+ });
681
+
682
+ addPlacement({
683
+ id: "workspaces.workspace.menu.members",
684
+ target: "workspace-tools:primary-menu",
685
+ surfaces: ["admin"],
686
+ order: 200,
687
+ componentToken: "workspaces.web.workspace-members.menu-item"
688
+ });
689
+ ```
690
+
691
+ That one block explains a lot of the new shell behavior.
692
+
693
+ - the authenticated profile menu can now switch into workspace surfaces
694
+ - the top-left area now has a workspace selector
695
+ - the top-right area can show a pending-invites cue
696
+ - the admin surface gets workspace tools in the top-right area
697
+ - the admin workspace settings menu is now another nested placement host with both `Settings` and `Members`
698
+
699
+ So the placement system from the shell chapter is still doing the same job as before. The app just has a richer routing and tenancy context now.
700
+
701
+ ### The local client provider gets one more app-owned token
702
+
703
+ `workspaces-web` also appends an app-owned component registration:
704
+
705
+ ```js
706
+ registerMainClientComponent("local.main.account.pending-invites.cue", () => AccountPendingInvitesCue);
707
+ ```
708
+
709
+ That is the pending-invites cue used in the shell when workspace invitations exist.
710
+
711
+ This is worth noticing because it follows the same app-owned token pattern the guide has shown before:
712
+
713
+ - the package installs an app-owned component file
714
+ - the app-local provider publishes it under a stable token
715
+ - placements can then render it through the shell
716
+
717
+ ## Under the hood
718
+
719
+ ### `workspaces-core` plugs into the users profile-sync registry
720
+
721
+ There is also an important server-side integration point that is easy to miss if you only look at routes and pages.
722
+
723
+ In the previous chapter, `users-core` introduced the tagged profile-sync lifecycle registry that runs after a JSKIT user record has been synchronized from auth. `workspaces-core` uses that seam by registering a contributor:
724
+
725
+ ```js
726
+ registerProfileSyncLifecycleContributor(app, "workspaces.core.profileSyncLifecycleContributor", (scope) => {
727
+ const workspaceService = scope.make("workspaces.service");
728
+
729
+ return Object.freeze({
730
+ contributorId: "workspaces.core.profileSync",
731
+ order: 100,
732
+ async afterIdentityProfileSynced({ profile, created, options } = {}) {
733
+ if (!created || !profile || typeof workspaceService?.provisionWorkspaceForNewUser !== "function") {
734
+ return;
735
+ }
736
+
737
+ await workspaceService.provisionWorkspaceForNewUser(profile, options);
738
+ }
739
+ });
740
+ });
741
+ ```
742
+
743
+ That means the workspace package does not need to patch auth directly to learn that a user was added. It listens through the `users-core` lifecycle registry instead.
744
+
745
+ For this chapter's `tenancyMode = "personal"` setup, that contributor now does real work. When a brand-new JSKIT user is synchronized from auth, `workspaces-core` uses the lifecycle seam to provision that user's first personal workspace automatically.
746
+
747
+ - `users-core` owns the "user was synchronized" lifecycle
748
+ - `workspaces-core` subscribes to that lifecycle through the registry
749
+ - tenancy policy decides whether the workspace layer provisions a personal workspace automatically
750
+
751
+ So even in this chapter, the package boundary is already correct: users owns user creation/sync, and workspaces reacts through an extension point.
752
+
753
+ ### Why this chapter is the real routing pivot
754
+
755
+ Earlier chapters added features inside a flat top-level app.
756
+
757
+ This chapter is different. It changes the shape of the app itself.
758
+
759
+ Before:
760
+
761
+ - global surfaces only
762
+ - no workspace slug in routes
763
+ - no workspace membership checks
764
+
765
+ After:
766
+
767
+ - global surfaces still exist
768
+ - workspace-scoped surfaces now exist too
769
+ - the shell can navigate between workspaces
770
+ - access to some surfaces now depends on workspace membership
771
+
772
+ That is why multi-homing deserves its own chapter. It is not just another feature package. It is the moment the app becomes tenancy-aware.
773
+
774
+ ## Summary
775
+
776
+ This chapter is the real routing and tenancy pivot in the guide.
777
+
778
+ - the app now uses `tenancyMode = "personal"`
779
+ - `workspaces-core` added the persistent schema and server runtime for workspaces
780
+ - `workspaces-web` added the first workspace-scoped surfaces, shell controls, and the workspace-owned account invites extension
781
+
782
+ At the end of this chapter, the app now has both:
783
+
784
+ - global surfaces such as `home`, `auth`, `account`, and `console`
785
+ - workspace-scoped surfaces such as `/w/[workspaceSlug]` and `/w/[workspaceSlug]/admin`
786
+
787
+ That is the most important mental shift to keep:
788
+
789
+ - earlier chapters added features inside one global app shell
790
+ - this chapter changed the topology of the app itself
791
+
792
+ From here on, later modules are no longer only adding pages or widgets. They can add features inside either:
793
+
794
+ - the global surfaces
795
+ - the workspace-specific surfaces