@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,822 @@
1
+ <!-- Generated by `npm run agent-docs:build` from `docs/guide/app-setup/database-layer.md`. Do not edit manually. -->
2
+
3
+ # Database layer
4
+
5
+ At the end of the previous chapter, the app could already authenticate real users through Supabase, but JSKIT was still using its no-database fallback for the app-side user mirror. In this chapter, we install the MySQL database runtime, add the migration tooling, and explain what that changes immediately and what it still does **not** change yet.
6
+
7
+ This chapter is more infrastructural than the previous ones. That is intentional. There is no dramatic new screen in the browser. The important change is that the app gains a real database layer that later packages can depend on.
8
+
9
+ ## Recap from previous chapters
10
+
11
+ To get back to the same starting point as the end of the previous chapter, run:
12
+
13
+ ```bash
14
+ SUPABASE_URL=...
15
+ SUPABASE_KEY=...
16
+
17
+ npx @jskit-ai/create-app exampleapp --tenancy-mode none
18
+ cd exampleapp
19
+ npm install
20
+
21
+ npx jskit add package shell-web
22
+ npx jskit add package auth-provider-supabase-core \
23
+ --auth-supabase-url "$SUPABASE_URL" \
24
+ --auth-supabase-publishable-key "$SUPABASE_KEY" \
25
+ --app-public-url "http://localhost:5173"
26
+
27
+ npx jskit add bundle auth-base
28
+ npm install
29
+ ```
30
+
31
+ If you are already continuing from the previous chapter, you are already in the right place and can skip that setup.
32
+
33
+ <DocsTerminalTip label="MySQL" title="Create The Database First">
34
+ Before installing the database runtime, make sure a real MySQL database already exists and that you know its connection details.
35
+
36
+ At minimum, keep these ready:
37
+
38
+ - host
39
+ - port
40
+ - database name
41
+ - database user
42
+ - database password
43
+
44
+ JSKIT installs the database runtime and writes those values into `.env`, but it does **not** create the MySQL server for you and it does **not** create the database itself.
45
+
46
+ If you are working locally, a very typical starting point is:
47
+
48
+ - host: `127.0.0.1`
49
+ - port: `3306`
50
+ - database name: `exampleapp`
51
+ - user: `exampleapp`
52
+
53
+ Using a normal app database user is usually a better guide setup than using the MySQL root account directly. It matches how production apps are normally configured, and it avoids local root-auth setups that work in the terminal but not through the Node driver.
54
+ </DocsTerminalTip>
55
+
56
+ ## Installing the database runtime
57
+
58
+ From inside `exampleapp`, run:
59
+
60
+ ```bash
61
+ DB_HOST=127.0.0.1
62
+ DB_PORT=3306
63
+ DB_NAME=exampleapp
64
+ DB_USER=exampleapp
65
+ DB_PASSWORD=secret
66
+
67
+ npx jskit add package database-runtime-mysql \
68
+ --db-host "$DB_HOST" \
69
+ --db-port "$DB_PORT" \
70
+ --db-name "$DB_NAME" \
71
+ --db-user "$DB_USER" \
72
+ --db-password "$DB_PASSWORD"
73
+ npm install
74
+ ```
75
+
76
+ The first command adds the MySQL driver package and its generic JSKIT database runtime dependency, using the explicit connection values from those `DB_*` variables:
77
+
78
+ - database host
79
+ - database port
80
+ - database name
81
+ - database user
82
+ - database password
83
+
84
+ The second command downloads the newly referenced runtime packages and the underlying Node dependencies, especially `knex` and `mysql2`.
85
+
86
+ If you open the app in the browser after this install, it still looks the same. That is expected.
87
+
88
+ - `/home` still renders the same shell
89
+ - `/auth/login` still renders the same login screen
90
+
91
+ This chapter changes the server-side infrastructure of the app, not the visible shell.
92
+
93
+ ## What changes immediately
94
+
95
+ Installing `database-runtime-mysql` gives the app three important things right away.
96
+
97
+ ### A real database runtime
98
+
99
+ The server can now build a real Knex client from environment variables. That gives later packages a standard way to ask for a database connection instead of each package inventing its own wiring.
100
+
101
+ ### A migration workflow
102
+
103
+ The app now gets three new scripts in `package.json`:
104
+
105
+ ```json
106
+ {
107
+ "scripts": {
108
+ "db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
109
+ "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
110
+ "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
111
+ }
112
+ }
113
+ ```
114
+
115
+ That is the first time the scaffold can talk about schema migrations in a standard way.
116
+
117
+ If you run the status command immediately after this chapter:
118
+
119
+ ```bash
120
+ npm run db:migrate:status
121
+ ```
122
+
123
+ you should still see that there are no completed migrations and no pending migration files yet. The runtime and the Knex wiring exist now, but no package has added real schema files until the next chapter.
124
+
125
+ ### A place for future schema files
126
+
127
+ The app also gets a top-level `migrations/` directory. Right now it only contains `.gitkeep`, which can look underwhelming at first, but that empty directory is actually useful. It means the migration toolchain is ready even before any package installs real schema files.
128
+
129
+ That is the key idea of this chapter:
130
+
131
+ - the database runtime provides the **infrastructure**
132
+ - later packages provide the **actual schema**
133
+
134
+ ## Two migration steps, two different tools
135
+
136
+ This chapter is the right place to make one distinction very explicit.
137
+
138
+ There are now two different migration-related layers in the app:
139
+
140
+ - JSKIT-managed migration files on disk
141
+ - Knex actually applying those files to the database
142
+
143
+ Those are **not** the same step.
144
+
145
+ ### `jskit migrations ...` writes managed migration files
146
+
147
+ If you run:
148
+
149
+ ```bash
150
+ npx jskit migrations changed
151
+ ```
152
+
153
+ JSKIT checks the installed package state in `.jskit/lock.json` and materializes any managed migration files that need to exist in `migrations/`.
154
+
155
+ That command is about the app scaffold on disk.
156
+
157
+ It does **not** run Knex against MySQL or Postgres.
158
+
159
+ At this point in the guide, that command will usually touch nothing yet, because `database-runtime-mysql` gives you the migration **tooling** and `migrations/.gitkeep`, but it does not add real schema files of its own.
160
+
161
+ ### `npm run db:migrate` runs Knex against the database
162
+
163
+ When you run:
164
+
165
+ ```bash
166
+ npm run db:migrate
167
+ ```
168
+
169
+ the Knex CLI reads `knexfile.js`, connects to the real database, and applies any pending migration files it finds.
170
+
171
+ That command is about the actual database.
172
+
173
+ So the clean mental model is:
174
+
175
+ 1. JSKIT writes or refreshes managed migration files into `migrations/`
176
+ 2. Knex applies those files to MySQL or Postgres
177
+
178
+ ### When you need each step
179
+
180
+ In many normal `jskit add package ...` flows, JSKIT already materializes a package's managed migration files while the package is being applied.
181
+
182
+ That means the most common flow is still:
183
+
184
+ ```bash
185
+ npx jskit add package users-web
186
+ npm install
187
+ npm run db:migrate
188
+ ```
189
+
190
+ But if you ever need JSKIT to re-materialize or refresh the managed migration files for installed packages, that is when you use:
191
+
192
+ ```bash
193
+ npx jskit migrations changed
194
+ npm run db:migrate
195
+ ```
196
+
197
+ So:
198
+
199
+ - use `jskit migrations ...` when you need JSKIT to write or refresh managed migration files
200
+ - use `npm run db:migrate` when you need Knex to apply pending migration files to the real database
201
+ - sometimes you need only `npm run db:migrate`
202
+ - sometimes, after repair or re-materialization work, you need **both**
203
+
204
+ ### Shared database helpers
205
+
206
+ The database layer also gives later server code a shared helper surface:
207
+
208
+ ```js
209
+ import {
210
+ applyVisibility,
211
+ applyVisibilityOwners,
212
+ toIsoString,
213
+ toInsertDateTime,
214
+ toNullableDateTime,
215
+ isDuplicateEntryError,
216
+ whereJsonTextEquals,
217
+ createWithTransaction
218
+ } from "@jskit-ai/database-runtime/shared";
219
+ ```
220
+
221
+ This is worth calling out now because the database layer is not only "Knex plus migrations".
222
+
223
+ It also gives your later repositories and services a standard persistence toolbox so every package does not have to solve the same problems differently. The main point is consistency:
224
+
225
+ - one way to format timestamps for database writes
226
+ - one way to format timestamps for API output
227
+ - one way to recognize duplicate-entry errors across databases
228
+ - one way to apply ownership-aware visibility filters to queries
229
+ - one way to stamp owner columns onto new rows from the current visibility context
230
+ - one way to build some dialect-aware JSON filters
231
+ - one small transaction helper pattern that packages can reuse
232
+
233
+ That helper layer comes from `@jskit-ai/database-runtime/shared`. The MySQL package mainly contributes the actual driver and dialect registration. The shared helper surface comes from the generic runtime package.
234
+
235
+ #### Visibility and ownership helpers
236
+
237
+ This is the main helper pair that matters once repositories start dealing with owned records.
238
+
239
+ In JSKIT persistence code, **visibility** means "which rows should this request be allowed to see or create?" It is the data-layer version of the same ownership model used by routes and CRUD resources:
240
+
241
+ - `public`
242
+ - the record is not scoped by owner columns
243
+ - `workspace`
244
+ - the record belongs to one workspace, usually through `workspace_id`
245
+ - `user`
246
+ - the record belongs to one user, usually through `user_id`
247
+ - `workspace_user`
248
+ - the record belongs to one workspace and one user together
249
+
250
+ That is why the shared helpers exist. Repositories should not have to re-implement the same ownership rules by hand every time they filter a query or build an insert payload.
251
+
252
+ The expected input is a **visibility context**. In practice this usually arrives from route/action execution context and gets passed down as `queryOptions.visibilityContext` or `callOptions.visibilityContext`.
253
+
254
+ The important shape looks like this:
255
+
256
+ ```js
257
+ const visibilityContext = {
258
+ visibility: "workspace_user",
259
+ scopeOwnerId: "42",
260
+ userId: "7"
261
+ };
262
+ ```
263
+
264
+ Those are the fields these helpers care about:
265
+
266
+ - `visibility`
267
+ - one of `public`, `workspace`, `user`, `workspace_user`
268
+ - `scopeOwnerId`
269
+ - the current workspace owner id
270
+ - `userId`
271
+ - the current user id
272
+
273
+ There are a couple of extra normalized visibility fields elsewhere in JSKIT, but for `applyVisibility(...)` and `applyVisibilityOwners(...)`, those three values are the main contract.
274
+
275
+ Use `applyVisibility(...)` when you are building a **read/update/delete query** that should only operate on rows visible to the current request.
276
+
277
+ ```js
278
+ import { applyVisibility } from "@jskit-ai/database-runtime/shared";
279
+
280
+ function listWorkspaceContacts(knex, queryOptions = {}) {
281
+ const query = knex("contacts").select("*").orderBy("created_at", "desc");
282
+ return applyVisibility(query, queryOptions.visibilityContext);
283
+ }
284
+ ```
285
+
286
+ If the current request is workspace-scoped:
287
+
288
+ ```js
289
+ const visibilityContext = {
290
+ visibility: "workspace",
291
+ scopeOwnerId: "12"
292
+ };
293
+ ```
294
+
295
+ the helper turns that into the equivalent of:
296
+
297
+ ```js
298
+ query.where("workspace_id", "12");
299
+ ```
300
+
301
+ That is the normal helper for workspace-owned rows.
302
+
303
+ The same pattern works for user-owned rows:
304
+
305
+ ```js
306
+ const query = knex("saved_views").select("*");
307
+ applyVisibility(query, {
308
+ visibility: "user",
309
+ userId: "7"
310
+ });
311
+ ```
312
+
313
+ which applies:
314
+
315
+ ```js
316
+ query.where("user_id", "7");
317
+ ```
318
+
319
+ And if a `workspace_user` context is required but only one owner id is available, the helper intentionally makes the query match nothing rather than accidentally leaking records. That is why these helpers are safer than scattering raw `where(...)` calls by hand.
320
+
321
+ Use `applyVisibilityOwners(...)` when you are building a **write payload** for a new owned row and you want JSKIT to stamp the owner columns from the current visibility context.
322
+
323
+ ```js
324
+ import { applyVisibilityOwners, toInsertDateTime } from "@jskit-ai/database-runtime/shared";
325
+
326
+ async function createWorkspaceNote(knex, payload, queryOptions = {}) {
327
+ const insertPayload = applyVisibilityOwners(
328
+ {
329
+ title: payload.title,
330
+ body: payload.body,
331
+ created_at: toInsertDateTime(),
332
+ updated_at: toInsertDateTime()
333
+ },
334
+ queryOptions.visibilityContext
335
+ );
336
+
337
+ await knex("notes").insert(insertPayload);
338
+ }
339
+ ```
340
+
341
+ If the request is workspace-scoped, the helper adds `workspace_id` automatically. If it is user-scoped, it adds `user_id`. If it is `workspace_user`, it adds both. That means repository code does **not** need to manually copy `workspace_id` and `user_id` out of the execution context for every insert.
342
+
343
+ That is especially useful for owned records such as:
344
+
345
+ - workspace-owned rows like `contacts`, `workspace_invites`, or `assistant_config`
346
+ - user-owned rows like personal settings or saved views
347
+ - combined workspace-and-user rows where both ids define ownership
348
+
349
+ So a `workspace_user` create can stay small:
350
+
351
+ ```js
352
+ const insertPayload = applyVisibilityOwners(
353
+ {
354
+ name: payload.name
355
+ },
356
+ {
357
+ visibility: "workspace_user",
358
+ scopeOwnerId: "4",
359
+ userId: "9"
360
+ }
361
+ );
362
+
363
+ // Result:
364
+ // {
365
+ // name: "...",
366
+ // workspace_id: "4",
367
+ // user_id: "9"
368
+ // }
369
+ ```
370
+
371
+ The important behavioral difference between the two helpers is:
372
+
373
+ - `applyVisibility(...)`
374
+ - scopes a query, and falls back to "no rows" when required owner ids are missing
375
+ - `applyVisibilityOwners(...)`
376
+ - stamps write payloads, and throws when required owner ids are missing
377
+
378
+ That split is deliberate. For reads, "match nothing" is the safe default. For writes, silently inserting an incorrectly owned row would be worse, so the helper fails fast instead.
379
+
380
+ If you want a slightly more structured repository shape, `createRepositoryScope(...)` from the same shared surface wraps these helpers into one small repository-scoping toolkit. But even when you write your own repositories manually, `applyVisibility(...)` and `applyVisibilityOwners(...)` are the key ownership helpers to reuse.
381
+
382
+ #### Date helpers
383
+
384
+ The date helpers are probably the first ones you are likely to reach for.
385
+
386
+ They exist because application code usually deals with dates in two different shapes:
387
+
388
+ - **database write shape**, such as a `DATETIME(3)`-style value
389
+ - **API/read shape**, usually an ISO timestamp string
390
+
391
+ Those are not the same thing, and it is easy for packages to become inconsistent if every repository hand-rolls the conversion.
392
+
393
+ Typical usage looks like this:
394
+
395
+ ```js
396
+ import {
397
+ toIsoString,
398
+ toInsertDateTime,
399
+ toNullableDateTime
400
+ } from "@jskit-ai/database-runtime/shared";
401
+
402
+ function mapRow(row) {
403
+ return {
404
+ id: String(row.id),
405
+ createdAt: toIsoString(row.created_at),
406
+ updatedAt: toIsoString(row.updated_at),
407
+ expiresAt: row.expires_at ? toIsoString(row.expires_at) : null
408
+ };
409
+ }
410
+
411
+ async function insertInvite(knex, payload) {
412
+ await knex("workspace_invites").insert({
413
+ email: payload.email,
414
+ expires_at: toNullableDateTime(payload.expiresAt),
415
+ created_at: toInsertDateTime(),
416
+ updated_at: toInsertDateTime()
417
+ });
418
+ }
419
+ ```
420
+
421
+ The reasoning behind each helper is:
422
+
423
+ - `toIsoString(...)`
424
+ - use this when data is coming **out** of the database and you want a stable ISO timestamp for application code or API responses
425
+ - `toInsertDateTime(...)`
426
+ - use this when you need "now" in the database's datetime text format for insert/update timestamps
427
+ - `toNullableDateTime(...)`
428
+ - use this when a field is optional and should become either a normalized database datetime or `null`
429
+
430
+ There is also a lower-level helper:
431
+
432
+ ```js
433
+ toDatabaseDateTimeUtc(value)
434
+ ```
435
+
436
+ That one is useful when you already know you are converting one specific date value into the database write format and you want to be explicit about it.
437
+
438
+ The main benefit is not convenience by itself. The real benefit is that packages stop inventing slightly different timestamp formats and timezone assumptions.
439
+
440
+ #### Duplicate-entry helpers
441
+
442
+ Another practical problem is unique-constraint handling.
443
+
444
+ MySQL and Postgres do not report duplicate-entry errors in exactly the same way. If every package checks raw driver error codes by hand, the code becomes repetitive and easy to get wrong.
445
+
446
+ The shared helper gives you one check:
447
+
448
+ ```js
449
+ import { isDuplicateEntryError } from "@jskit-ai/database-runtime/shared";
450
+
451
+ try {
452
+ await knex("users").insert({
453
+ email: payload.email
454
+ });
455
+ } catch (error) {
456
+ if (!isDuplicateEntryError(error)) {
457
+ throw error;
458
+ }
459
+
460
+ // Handle the "already exists" case here.
461
+ }
462
+ ```
463
+
464
+ Why this is useful:
465
+
466
+ - it keeps repository code cleaner
467
+ - it avoids scattering vendor-specific numeric/string error codes everywhere
468
+ - it makes later database portability less painful
469
+
470
+ #### JSON query helpers
471
+
472
+ Some databases expose different SQL syntax for reading JSON fields.
473
+
474
+ The shared helpers smooth over a small but useful part of that difference. For example:
475
+
476
+ ```js
477
+ import { whereJsonTextEquals } from "@jskit-ai/database-runtime/shared";
478
+
479
+ const query = knex("contacts");
480
+ whereJsonTextEquals(query, {
481
+ column: "metadata",
482
+ path: "status",
483
+ value: "active"
484
+ });
485
+ ```
486
+
487
+ That helper uses the right JSON text expression for the active client instead of forcing every package to write separate MySQL and Postgres raw SQL.
488
+
489
+ This is not a full ORM abstraction layer, and it should not be described that way. It is just a small set of helpers for the database differences JSKIT packages actually care about.
490
+
491
+ #### Transaction and repository helpers
492
+
493
+ The shared surface also includes a few small helpers for transaction-friendly repository code:
494
+
495
+ ```js
496
+ import { createWithTransaction } from "@jskit-ai/database-runtime/shared";
497
+
498
+ function createRepository(knex) {
499
+ const withTransaction = createWithTransaction(knex);
500
+
501
+ return {
502
+ withTransaction
503
+ };
504
+ }
505
+ ```
506
+
507
+ That pattern shows up in JSKIT packages because it gives repositories a simple standard way to say, "run this unit of work inside a transaction".
508
+
509
+ You do not need to memorize all of these helpers yet. The important thing to understand in this chapter is just that the database layer gives you:
510
+
511
+ - runtime wiring
512
+ - migration tooling
513
+ - a shared persistence utility surface for later packages and app code
514
+
515
+ ## What this still does not change yet
516
+
517
+ Installing the database runtime is important, but it is not the same thing as installing the full users/account data model.
518
+
519
+ Right now, after this chapter:
520
+
521
+ - the app can resolve database settings from `.env`
522
+ - the server can create a Knex client
523
+ - the app can run migration commands
524
+ - later packages are allowed to depend on `runtime.database`
525
+
526
+ But the app still does **not** have:
527
+
528
+ - JSKIT user tables
529
+ - JSKIT user settings tables
530
+ - persistent account/profile rows on the JSKIT side
531
+ - workspace tables
532
+ - CRUD tables of its own
533
+
534
+ That means authentication is still only **partly** database-backed.
535
+
536
+ - Supabase is still the real source of truth for auth users and sessions.
537
+ - JSKIT still has a database runtime available.
538
+ - But JSKIT still has **no installed package yet** that tells auth to switch from the standalone in-memory profile mirror to the persistent users-backed one.
539
+
540
+ So this chapter is an infrastructure step. It makes the database layer available, but it does not yet install the package that uses that layer for persistent JSKIT-side user data.
541
+
542
+ <DocsTerminalTip label="Important" title="Auth Is Still In Standalone Mode">
543
+ This is the most important thing to keep straight:
544
+
545
+ - adding `database-runtime-mysql` does **not** automatically make the auth chapter's temporary mirror disappear
546
+ - it also does **not** create JSKIT user rows yet
547
+
548
+ That only changes later, when a package such as `users-core` is installed and tells auth to use the persistent users-backed profile sync mode.
549
+
550
+ So after this chapter the app has a database layer, but authentication still behaves like:
551
+
552
+ - real Supabase auth
553
+ - temporary JSKIT-side mirror
554
+
555
+ not yet:
556
+
557
+ - real Supabase auth
558
+ - persistent JSKIT-side users layer
559
+ </DocsTerminalTip>
560
+
561
+ ## Under the hood
562
+
563
+ The interesting files for this chapter are mostly at the top level:
564
+
565
+ ```text
566
+ exampleapp/
567
+ .env
568
+ knexfile.js
569
+ migrations/
570
+ .gitkeep
571
+ package.json
572
+ ```
573
+
574
+ This is the first chapter where the new behavior is mostly about server infrastructure rather than pages or client layouts.
575
+
576
+ ### `package.json` gains database dependencies and scripts
577
+
578
+ After installing the MySQL runtime, the important new pieces in `package.json` look like this:
579
+
580
+ ```json
581
+ {
582
+ "dependencies": {
583
+ "@jskit-ai/database-runtime": "0.x",
584
+ "@jskit-ai/database-runtime-mysql": "0.x",
585
+ "knex": "^3.1.0",
586
+ "mysql2": "^3.11.2"
587
+ },
588
+ "scripts": {
589
+ "db:migrate": "knex --knexfile ./knexfile.js migrate:latest",
590
+ "db:migrate:rollback": "knex --knexfile ./knexfile.js migrate:rollback",
591
+ "db:migrate:status": "knex --knexfile ./knexfile.js migrate:list"
592
+ }
593
+ }
594
+ ```
595
+
596
+ Those new dependencies divide into two roles:
597
+
598
+ - `@jskit-ai/database-runtime` is the generic JSKIT database runtime
599
+ - `@jskit-ai/database-runtime-mysql` is the MySQL-specific driver package
600
+ - `knex` is the database toolkit used by both runtime code and migration commands
601
+ - `mysql2` is the actual Node driver that speaks to MySQL
602
+
603
+ The three new scripts are also worth reading carefully:
604
+
605
+ - `db:migrate` applies all pending migrations
606
+ - `db:migrate:rollback` rolls back the last migration batch
607
+ - `db:migrate:status` lists applied and pending migrations
608
+
609
+ They are not special JSKIT commands. They are ordinary project scripts, which makes them easy to run in any environment.
610
+
611
+ ### `.env` now owns the database connection settings
612
+
613
+ The package install also writes the database settings into `.env`:
614
+
615
+ ```dotenv
616
+ DB_CLIENT=mysql2
617
+ DB_HOST=127.0.0.1
618
+ DB_PORT=3306
619
+ DB_NAME=exampleapp
620
+ DB_USER=exampleapp
621
+ DB_PASSWORD=secret
622
+ ```
623
+
624
+ That small block is doing two jobs.
625
+
626
+ - `DB_CLIENT` tells the generic database runtime which dialect was installed.
627
+ - the rest of the variables describe the real connection to MySQL.
628
+
629
+ This matters because the generic runtime is written to support more than one driver package. The runtime does not hard-code MySQL. It reads the configured client and the installed driver and checks that they agree.
630
+
631
+ ### `knexfile.js` is for migration commands, not normal page code
632
+
633
+ The migration scripts in `package.json` work because the app now has a top-level `knexfile.js`:
634
+
635
+ ```js
636
+ import path from "node:path";
637
+ import dotenv from "dotenv";
638
+ import {
639
+ normalizeText,
640
+ toKnexClientId,
641
+ resolveDatabaseClientFromEnvironment,
642
+ resolveKnexConnectionFromEnvironment
643
+ } from "@jskit-ai/database-runtime/shared";
644
+
645
+ const appRoot = process.cwd();
646
+ dotenv.config({
647
+ path: path.join(appRoot, ".env"),
648
+ quiet: true
649
+ });
650
+
651
+ const dialectId = resolveDatabaseClientFromEnvironment(process.env);
652
+ const client = toKnexClientId(dialectId);
653
+ const defaultPort = dialectId === "pg" ? 5432 : 3306;
654
+ const migrationsDirectory = path.resolve(appRoot, normalizeText(process.env.DB_MIGRATIONS_DIR) || "migrations");
655
+
656
+ export default {
657
+ client,
658
+ connection: resolveKnexConnectionFromEnvironment(process.env, {
659
+ client: dialectId,
660
+ defaultPort,
661
+ context: "knex migrations"
662
+ }),
663
+ migrations: {
664
+ directory: migrationsDirectory,
665
+ extension: "cjs"
666
+ }
667
+ };
668
+ ```
669
+
670
+ The important thing to understand is what this file is **for**.
671
+
672
+ It is not the main runtime API that your app code imports during a request. It is the configuration file the Knex CLI reads when you run commands such as:
673
+
674
+ ```bash
675
+ npm run db:migrate
676
+ ```
677
+
678
+ So there are really two separate database entry points now:
679
+
680
+ - `knexfile.js` for migration commands
681
+ - the JSKIT server provider runtime for application code
682
+
683
+ That separation is good. It keeps the operational CLI workflow and the app runtime wiring clear.
684
+
685
+ ### The MySQL package registers the driver, and the generic runtime builds the Knex client
686
+
687
+ On the server side, the two installed packages split responsibilities very deliberately.
688
+
689
+ The MySQL-specific package registers a driver token:
690
+
691
+ ```js
692
+ class DatabaseRuntimeMysqlServiceProvider {
693
+ static id = "runtime.database.driver.mysql";
694
+
695
+ register(app) {
696
+ app.singleton("runtime.database.driver.mysql", () => MYSQL_DATABASE_DRIVER_API);
697
+ }
698
+ }
699
+ ```
700
+
701
+ That does **not** create the database client yet. It only tells the app, "a MySQL driver is available, and here is its dialect metadata."
702
+
703
+ The generic runtime then uses that driver to create the real Knex wiring:
704
+
705
+ ```js
706
+ class DatabaseRuntimeServiceProvider {
707
+ static id = "runtime.database";
708
+
709
+ register(app) {
710
+ app.singleton("runtime.database", () => DATABASE_RUNTIME_SERVER_API);
711
+
712
+ if (!app.has("runtime.database.driver")) {
713
+ app.singleton("runtime.database.driver", (scope) => resolveSingleRegisteredDriver(scope));
714
+ }
715
+
716
+ if (!app.has("jskit.database.knex")) {
717
+ app.singleton("jskit.database.knex", (scope) => createKnexInstance(scope));
718
+ }
719
+
720
+ if (!app.has("jskit.database.transactionManager")) {
721
+ app.singleton("jskit.database.transactionManager", (scope) => {
722
+ const knex = scope.make("jskit.database.knex");
723
+ return createTransactionManager({ knex });
724
+ });
725
+ }
726
+ }
727
+ }
728
+ ```
729
+
730
+ That one provider is the real center of this chapter. It gives later server code a standard set of container tokens:
731
+
732
+ - `runtime.database`
733
+ - `runtime.database.driver`
734
+ - `jskit.database.knex`
735
+ - `jskit.database.transactionManager`
736
+
737
+ This is why later packages can simply say "I require `runtime.database`" instead of building their own database bootstrap.
738
+
739
+ ### Why the browser still feels unchanged
740
+
741
+ At first glance it can feel strange that the database layer is installed but the app still behaves almost exactly like the previous chapter.
742
+
743
+ The reason is simple:
744
+
745
+ - the runtime is now available
746
+ - but almost no installed package is using it yet
747
+
748
+ Right now:
749
+
750
+ - `shell-web` is still a shell/layout package
751
+ - `auth-web` is still a web auth package
752
+ - `auth-provider-supabase-core` is still talking to Supabase for the real auth work
753
+
754
+ So the app has gained a new capability, but no visible part of the UI depends on that capability yet.
755
+
756
+ ### Why auth still uses the standalone profile mirror
757
+
758
+ This is the most important code path to read in this chapter.
759
+
760
+ Inside `AuthSupabaseServiceProvider`, auth still resolves its profile mode from the environment:
761
+
762
+ ```js
763
+ const authProfileMode = resolveAuthProfileMode(env);
764
+ let userProfileSyncService = fallbackStandaloneProfileSyncService;
765
+
766
+ if (authProfileMode === AUTH_PROFILE_MODE_USERS) {
767
+ if (!scope.has("users.profile.sync.service")) {
768
+ throw new Error(
769
+ "AuthSupabaseServiceProvider requires users.profile.sync.service when AUTH_PROFILE_MODE=users."
770
+ );
771
+ }
772
+ userProfileSyncService = scope.make("users.profile.sync.service");
773
+ }
774
+ ```
775
+
776
+ That snippet explains the whole consequence of this chapter.
777
+
778
+ - The default mode is still `standalone`.
779
+ - The fallback service is still the in-memory profile sync service from the previous chapter.
780
+ - Nothing in `database-runtime-mysql` changes `AUTH_PROFILE_MODE`.
781
+ - Nothing in `database-runtime-mysql` provides `users.profile.sync.service`.
782
+
783
+ So the auth layer keeps behaving the same way it did before:
784
+
785
+ - Supabase still owns the real auth user and session
786
+ - JSKIT still mirrors just enough profile data locally
787
+ - that JSKIT-side mirror is still not persistent yet
788
+
789
+ The database runtime is now ready, but the users layer that will actually use it has not been installed yet.
790
+
791
+ ### Why the empty `migrations/` directory is important
792
+
793
+ The new `migrations/` directory can look almost silly at first because it only contains `.gitkeep`. But that empty directory is the cleanest signal of what this chapter really does.
794
+
795
+ It means:
796
+
797
+ - the app now has a migration system
798
+ - the app does **not** yet have a schema of its own
799
+
800
+ That is exactly the right state at this stage of the guide.
801
+
802
+ The database runtime chapter should give the app a database foundation first. The next data-heavy chapters can then install actual schema migrations on top of that foundation.
803
+
804
+ ## Summary
805
+
806
+ This chapter did not make the app feel dramatically different in the browser, but it changed the server foundation in an important way.
807
+
808
+ - the app now has a real JSKIT database runtime
809
+ - the app now has a standard Knex migration workflow
810
+ - the app now has a place for future schema files
811
+
812
+ But just as importantly, this chapter also defined what has **not** changed yet:
813
+
814
+ - auth still uses the standalone JSKIT-side mirror
815
+ - JSKIT still has no persistent users layer of its own
816
+ - no feature package has started storing real app data yet
817
+
818
+ So the right mental model at the end of this chapter is:
819
+
820
+ - Supabase already handles real authentication
821
+ - MySQL is now wired up and ready
822
+ - the persistent JSKIT-side user model arrives in the next chapter