@korajs/cli 1.0.0-beta.11 → 1.0.0-beta.13

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 (83) hide show
  1. package/README.md +25 -107
  2. package/dist/bin.cjs +808 -340
  3. package/dist/bin.cjs.map +1 -1
  4. package/dist/bin.js +588 -174
  5. package/dist/bin.js.map +1 -1
  6. package/dist/{chunk-G2PBYMMA.js → chunk-7Y4J2GXP.js} +168 -153
  7. package/dist/chunk-7Y4J2GXP.js.map +1 -0
  8. package/dist/{chunk-Z7GCXMGW.js → chunk-TIKGLNER.js} +44 -9
  9. package/dist/chunk-TIKGLNER.js.map +1 -0
  10. package/dist/{chunk-4IKCHRJ6.js → chunk-TVIRMDER.js} +122 -112
  11. package/dist/chunk-TVIRMDER.js.map +1 -0
  12. package/dist/create.cjs +63 -28
  13. package/dist/create.cjs.map +1 -1
  14. package/dist/create.js +2 -2
  15. package/dist/index.cjs +41 -14
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +21 -1
  18. package/dist/index.d.ts +21 -1
  19. package/dist/index.js +8 -2
  20. package/dist/{studio-server-NIFKZI4F.js → studio-server-XLGZNGG5.js} +11 -2
  21. package/dist/studio-server-XLGZNGG5.js.map +1 -0
  22. package/dist/vite.cjs +351 -0
  23. package/dist/vite.cjs.map +1 -0
  24. package/dist/vite.d.cts +161 -0
  25. package/dist/vite.d.ts +161 -0
  26. package/dist/vite.js +321 -0
  27. package/dist/vite.js.map +1 -0
  28. package/package.json +18 -8
  29. package/templates/react-basic/AGENTS.md +5 -5
  30. package/templates/react-basic/vite.config.ts +4 -1
  31. package/templates/react-sync/AGENTS.md +5 -5
  32. package/templates/react-sync/server.ts +8 -0
  33. package/templates/react-sync/src/vite-env.d.ts +1 -0
  34. package/templates/react-sync/vite.config.ts +4 -1
  35. package/templates/react-tailwind/AGENTS.md +5 -5
  36. package/templates/react-tailwind/vite.config.ts +10 -1
  37. package/templates/react-tailwind-sync/AGENTS.md +9 -7
  38. package/templates/react-tailwind-sync/server.ts +8 -0
  39. package/templates/react-tailwind-sync/src/App.tsx +1 -1
  40. package/templates/react-tailwind-sync/src/kora.ts +32 -0
  41. package/templates/react-tailwind-sync/src/main.tsx +1 -23
  42. package/templates/react-tailwind-sync/src/modules/todos/todo.mutations.ts +4 -8
  43. package/templates/react-tailwind-sync/src/modules/todos/todo.queries.ts +2 -2
  44. package/templates/react-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  45. package/templates/react-tailwind-sync/src/vite-env.d.ts +1 -0
  46. package/templates/react-tailwind-sync/vite.config.ts +10 -1
  47. package/templates/svelte-basic/AGENTS.md +4 -4
  48. package/templates/svelte-basic/package.json.hbs +6 -0
  49. package/templates/svelte-basic/src/App.svelte +2 -1
  50. package/templates/svelte-basic/vite.config.ts +4 -1
  51. package/templates/svelte-sync/AGENTS.md +4 -4
  52. package/templates/svelte-sync/server.ts +8 -0
  53. package/templates/svelte-sync/src/App.svelte +2 -1
  54. package/templates/svelte-sync/vite.config.ts +4 -1
  55. package/templates/svelte-tailwind/AGENTS.md +4 -4
  56. package/templates/svelte-tailwind/package.json.hbs +7 -1
  57. package/templates/svelte-tailwind/src/App.svelte +2 -1
  58. package/templates/svelte-tailwind/vite.config.ts +10 -1
  59. package/templates/svelte-tailwind-sync/AGENTS.md +4 -4
  60. package/templates/svelte-tailwind-sync/package.json.hbs +1 -1
  61. package/templates/svelte-tailwind-sync/server.ts +8 -0
  62. package/templates/svelte-tailwind-sync/src/App.svelte +2 -1
  63. package/templates/svelte-tailwind-sync/vite.config.ts +10 -1
  64. package/templates/tauri-react/AGENTS.md +5 -5
  65. package/templates/tauri-react/package.json.hbs +1 -1
  66. package/templates/tauri-react/server.ts +8 -0
  67. package/templates/tauri-react/src/vite-env.d.ts +1 -0
  68. package/templates/vue-basic/AGENTS.md +4 -4
  69. package/templates/vue-basic/package.json.hbs +6 -0
  70. package/templates/vue-basic/vite.config.ts +4 -1
  71. package/templates/vue-sync/AGENTS.md +4 -4
  72. package/templates/vue-sync/server.ts +8 -0
  73. package/templates/vue-sync/vite.config.ts +4 -1
  74. package/templates/vue-tailwind/AGENTS.md +4 -4
  75. package/templates/vue-tailwind/package.json.hbs +6 -0
  76. package/templates/vue-tailwind/vite.config.ts +4 -1
  77. package/templates/vue-tailwind-sync/AGENTS.md +4 -4
  78. package/templates/vue-tailwind-sync/server.ts +8 -0
  79. package/templates/vue-tailwind-sync/vite.config.ts +4 -1
  80. package/dist/chunk-4IKCHRJ6.js.map +0 -1
  81. package/dist/chunk-G2PBYMMA.js.map +0 -1
  82. package/dist/chunk-Z7GCXMGW.js.map +0 -1
  83. package/dist/studio-server-NIFKZI4F.js.map +0 -1
@@ -13,17 +13,23 @@
13
13
  "korajs": "{{koraVersion}}",
14
14
  "@korajs/svelte": "{{koraVersion}}",
15
15
  "@korajs/store": "{{koraVersion}}",
16
- "@lucide/svelte": "^0.468.0",
16
+ "@lucide/svelte": "^0.479.0",
17
17
  "@sqlite.org/sqlite-wasm": ">=3.51.0-build1",
18
18
  "svelte": "^5.0.0",
19
19
  "yjs": "^13.6.30"
20
20
  },
21
+ "pnpm": {
22
+ "onlyBuiltDependencies": [
23
+ "esbuild"
24
+ ]
25
+ },
21
26
  "devDependencies": {
22
27
  "@korajs/cli": "{{koraVersion}}",
23
28
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
24
29
  "@tailwindcss/vite": "^4.0.0",
25
30
  "svelte-check": "^4.1.0",
26
31
  "tailwindcss": "^4.0.0",
32
+ "tsx": "^4.19.0",
27
33
  "typescript": "^5.7.0",
28
34
  "vite": "^6.0.0"
29
35
  }
@@ -7,7 +7,8 @@ type Filter = 'all' | 'active' | 'completed'
7
7
  const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } =
8
8
  createTodosStores()
9
9
 
10
- const filter = $state<Filter>('all')
10
+ // biome-ignore lint/style/useConst: reassigned in the markup, which Biome does not see
11
+ let filter = $state<Filter>('all')
11
12
  let input = $state('')
12
13
 
13
14
  const filteredTodos = $derived(
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import { svelte } from '@sveltejs/vite-plugin-svelte'
4
5
  import tailwindcss from '@tailwindcss/vite'
5
6
  import type { Plugin } from 'vite'
@@ -44,8 +45,16 @@ function sqliteWasmHotfix(): Plugin {
44
45
  }
45
46
  }
46
47
 
48
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
49
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
47
50
  export default defineConfig({
48
- plugins: [svelte(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
51
+ plugins: [
52
+ svelte(),
53
+ tailwindcss(),
54
+ crossOriginIsolation(),
55
+ sqliteWasmHotfix(),
56
+ koraServiceWorker(),
57
+ ],
49
58
  worker: { format: 'es' },
50
59
  optimizeDeps: {
51
60
  exclude: ['@sqlite.org/sqlite-wasm', '@korajs/store'],
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -73,4 +73,4 @@ Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })
73
73
 
74
74
  ## Svelte bindings
75
75
 
76
- Kora's Svelte package exposes store-based bindings from `@korajs/svelte`, wired through the provider set up in the app entry. Queries are reactive stores that read synchronously from the local database; mutations expose error state that must be surfaced in the UI. Avoid loading spinners for local reads.
76
+ Kora's Svelte package exposes store-based bindings from `@korajs/svelte`, wired through the provider set up in the app entry. Queries are reactive stores over the local database (no network, no spinner; `[]` until the first local result); mutations expose error state that must be surfaced in the UI.
@@ -16,7 +16,7 @@
16
16
  "@korajs/auth": "{{koraVersion}}",
17
17
  "@korajs/svelte": "{{koraVersion}}",
18
18
  "@korajs/store": "{{koraVersion}}",
19
- "@lucide/svelte": "^0.468.0",
19
+ "@lucide/svelte": "^0.479.0",
20
20
  "@sqlite.org/sqlite-wasm": ">=3.51.0-build1",
21
21
  "svelte": "^5.0.0",
22
22
  "ws": "^8.18.0",
@@ -3,7 +3,9 @@ import {
3
3
  type CreateKoraAuthServerOptions,
4
4
  createKoraAuthServer,
5
5
  createPostgresOAuthStores,
6
+ createPostgresUserStore,
6
7
  createSqliteOAuthStores,
8
+ createSqliteUserStore,
7
9
  googleProvider,
8
10
  } from '@korajs/auth/server'
9
11
  import {
@@ -101,8 +103,14 @@ async function createAuth() {
101
103
  }
102
104
 
103
105
  const oauth = await createOAuthConfig()
106
+ // Users and token revocations persist in the same database (the user store's
107
+ // revocation store is used automatically), so sign-outs survive restarts.
108
+ const userStore = process.env.DATABASE_URL
109
+ ? await createPostgresUserStore({ connectionString: process.env.DATABASE_URL })
110
+ : await createSqliteUserStore({ filename: process.env.KORA_AUTH_DB || './.kora/kora-auth.db' })
104
111
  return createKoraAuthServer({
105
112
  jwtSecret: process.env.KORA_AUTH_SECRET,
113
+ userStore,
106
114
  ...(oauth ? { oauth } : {}),
107
115
  })
108
116
  }
@@ -21,7 +21,8 @@ const auth = createAuthStore()
21
21
  const todos = createTodosStores()
22
22
  const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = todos
23
23
 
24
- const filter = $state<Filter>('all')
24
+ // biome-ignore lint/style/useConst: reassigned in the markup, which Biome does not see
25
+ let filter = $state<Filter>('all')
25
26
  let input = $state('')
26
27
 
27
28
  const filteredTodos = $derived(
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import { svelte } from '@sveltejs/vite-plugin-svelte'
4
5
  import tailwindcss from '@tailwindcss/vite'
5
6
  import type { Plugin } from 'vite'
@@ -57,8 +58,16 @@ function sqliteWasmHotfix(): Plugin {
57
58
  }
58
59
  }
59
60
 
61
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
62
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
60
63
  export default defineConfig({
61
- plugins: [svelte(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
64
+ plugins: [
65
+ svelte(),
66
+ tailwindcss(),
67
+ crossOriginIsolation(),
68
+ sqliteWasmHotfix(),
69
+ koraServiceWorker(),
70
+ ],
62
71
  worker: {
63
72
  format: 'es',
64
73
  },
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -77,14 +77,14 @@ Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })
77
77
  import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
78
78
 
79
79
  const todos = useCollection('todos')
80
- const rows = useQuery(orderedTodos(todos)) // reactive, no loading state for local data
80
+ const rows = useQuery(orderedTodos(todos)) // reactive; [] on the very first render
81
81
  const addTodo = useMutation((data) => todos.insert(data))
82
82
  addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
83
83
  await addTodo.mutateAsync({ title: 'x' }) // throws on failure; use when you need the error
84
84
  const status = useSyncStatus() // status.status, status.pendingOperations
85
85
  ```
86
86
 
87
- Rules: always render `mutation.error` somewhere. The app must work under React StrictMode (the framework's hooks are StrictMode-safe; keep `<StrictMode>` in `src/main.tsx`). `useQuery` returns data synchronously from the local store, so do not add loading spinners for local reads.
87
+ Rules: always render `mutation.error` somewhere. The app must work under React StrictMode (the framework's hooks are StrictMode-safe; keep `<StrictMode>` in `src/main.tsx`). `useQuery` reads the local store: no network, no spinner. Its very first render returns `[]` before the local query has run; use `useQueryState` when you must tell "loading" from "no rows".
88
88
 
89
89
  ## Tauri specifics
90
90
 
@@ -16,7 +16,7 @@
16
16
  "@korajs/auth": "{{koraVersion}}",
17
17
  "@korajs/core": "{{koraVersion}}",
18
18
  "@korajs/react": "{{koraVersion}}",
19
- "@korajs/tauri": "{{koraVersion}}",
19
+ "@korajs/tauri": "{{tauriVersion}}",
20
20
  "@tauri-apps/api": "^2.0.0",
21
21
  "@tauri-apps/plugin-updater": "^2.0.0",
22
22
  "lucide-react": "^0.468.0",
@@ -3,7 +3,9 @@ import {
3
3
  type CreateKoraAuthServerOptions,
4
4
  createKoraAuthServer,
5
5
  createPostgresOAuthStores,
6
+ createPostgresUserStore,
6
7
  createSqliteOAuthStores,
8
+ createSqliteUserStore,
7
9
  googleProvider,
8
10
  } from '@korajs/auth/server'
9
11
  import {
@@ -108,8 +110,14 @@ async function createAuth() {
108
110
  }
109
111
 
110
112
  const oauth = await createOAuthConfig()
113
+ // Users and token revocations persist in the same database (the user store's
114
+ // revocation store is used automatically), so sign-outs survive restarts.
115
+ const userStore = process.env.DATABASE_URL
116
+ ? await createPostgresUserStore({ connectionString: process.env.DATABASE_URL })
117
+ : await createSqliteUserStore({ filename: process.env.KORA_AUTH_DB || './.kora/kora-auth.db' })
111
118
  return createKoraAuthServer({
112
119
  jwtSecret: process.env.KORA_AUTH_SECRET,
120
+ userStore,
113
121
  ...(oauth ? { oauth } : {}),
114
122
  })
115
123
  }
@@ -1,6 +1,7 @@
1
1
  interface ImportMetaEnv {
2
2
  readonly DEV: boolean
3
3
  readonly VITE_SYNC_URL?: string
4
+ readonly VITE_AUTH_URL?: string
4
5
  }
5
6
 
6
7
  interface ImportMeta {
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -81,4 +81,4 @@ const rows = useQuery(orderedTodos(todos)) // readonly reactive ref
81
81
  const addTodo = useMutation((data) => todos.insert(data))
82
82
  ```
83
83
 
84
- Rules: always surface the mutation's error state in the UI. `useQuery` reads synchronously from the local store, so avoid loading spinners for local data. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
84
+ Rules: always surface the mutation's error state in the UI. `useQuery` reads the local store (no network, no spinner); the ref holds `[]` until the first local result. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
@@ -16,9 +16,15 @@
16
16
  "vue": "^3.5.0",
17
17
  "yjs": "^13.6.30"
18
18
  },
19
+ "pnpm": {
20
+ "onlyBuiltDependencies": [
21
+ "esbuild"
22
+ ]
23
+ },
19
24
  "devDependencies": {
20
25
  "@korajs/cli": "{{koraVersion}}",
21
26
  "@vitejs/plugin-vue": "^5.2.0",
27
+ "tsx": "^4.19.0",
22
28
  "typescript": "^5.7.0",
23
29
  "vite": "^6.0.0",
24
30
  "vue-tsc": "^2.2.0"
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import vue from '@vitejs/plugin-vue'
4
5
  import type { Plugin } from 'vite'
5
6
  import { defineConfig } from 'vite'
@@ -56,8 +57,10 @@ function sqliteWasmHotfix(): Plugin {
56
57
  }
57
58
  }
58
59
 
60
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
61
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
59
62
  export default defineConfig({
60
- plugins: [vue(), crossOriginIsolation(), sqliteWasmHotfix()],
63
+ plugins: [vue(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
61
64
  worker: {
62
65
  format: 'es',
63
66
  },
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -81,4 +81,4 @@ const rows = useQuery(orderedTodos(todos)) // readonly reactive ref
81
81
  const addTodo = useMutation((data) => todos.insert(data))
82
82
  ```
83
83
 
84
- Rules: always surface the mutation's error state in the UI. `useQuery` reads synchronously from the local store, so avoid loading spinners for local data. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
84
+ Rules: always surface the mutation's error state in the UI. `useQuery` reads the local store (no network, no spinner); the ref holds `[]` until the first local result. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
@@ -3,7 +3,9 @@ import {
3
3
  type CreateKoraAuthServerOptions,
4
4
  createKoraAuthServer,
5
5
  createPostgresOAuthStores,
6
+ createPostgresUserStore,
6
7
  createSqliteOAuthStores,
8
+ createSqliteUserStore,
7
9
  googleProvider,
8
10
  } from '@korajs/auth/server'
9
11
  import {
@@ -101,8 +103,14 @@ async function createAuth() {
101
103
  }
102
104
 
103
105
  const oauth = await createOAuthConfig()
106
+ // Users and token revocations persist in the same database (the user store's
107
+ // revocation store is used automatically), so sign-outs survive restarts.
108
+ const userStore = process.env.DATABASE_URL
109
+ ? await createPostgresUserStore({ connectionString: process.env.DATABASE_URL })
110
+ : await createSqliteUserStore({ filename: process.env.KORA_AUTH_DB || './.kora/kora-auth.db' })
104
111
  return createKoraAuthServer({
105
112
  jwtSecret: process.env.KORA_AUTH_SECRET,
113
+ userStore,
106
114
  ...(oauth ? { oauth } : {}),
107
115
  })
108
116
  }
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import vue from '@vitejs/plugin-vue'
4
5
  import type { Plugin } from 'vite'
5
6
  import { defineConfig } from 'vite'
@@ -56,8 +57,10 @@ function sqliteWasmHotfix(): Plugin {
56
57
  }
57
58
  }
58
59
 
60
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
61
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
59
62
  export default defineConfig({
60
- plugins: [vue(), crossOriginIsolation(), sqliteWasmHotfix()],
63
+ plugins: [vue(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
61
64
  worker: {
62
65
  format: 'es',
63
66
  },
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -81,4 +81,4 @@ const rows = useQuery(orderedTodos(todos)) // readonly reactive ref
81
81
  const addTodo = useMutation((data) => todos.insert(data))
82
82
  ```
83
83
 
84
- Rules: always surface the mutation's error state in the UI. `useQuery` reads synchronously from the local store, so avoid loading spinners for local data. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
84
+ Rules: always surface the mutation's error state in the UI. `useQuery` reads the local store (no network, no spinner); the ref holds `[]` until the first local result. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
@@ -17,11 +17,17 @@
17
17
  "vue": "^3.5.0",
18
18
  "yjs": "^13.6.30"
19
19
  },
20
+ "pnpm": {
21
+ "onlyBuiltDependencies": [
22
+ "esbuild"
23
+ ]
24
+ },
20
25
  "devDependencies": {
21
26
  "@korajs/cli": "{{koraVersion}}",
22
27
  "@tailwindcss/vite": "^4.0.0",
23
28
  "@vitejs/plugin-vue": "^5.2.0",
24
29
  "tailwindcss": "^4.0.0",
30
+ "tsx": "^4.19.0",
25
31
  "typescript": "^5.7.0",
26
32
  "vite": "^6.0.0",
27
33
  "vue-tsc": "^2.2.0"
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import tailwindcss from '@tailwindcss/vite'
4
5
  import vue from '@vitejs/plugin-vue'
5
6
  import type { Plugin } from 'vite'
@@ -57,8 +58,10 @@ function sqliteWasmHotfix(): Plugin {
57
58
  }
58
59
  }
59
60
 
61
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
62
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
60
63
  export default defineConfig({
61
- plugins: [vue(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
64
+ plugins: [vue(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
62
65
  worker: {
63
66
  format: 'es',
64
67
  },
@@ -12,7 +12,7 @@ This is a **Kora.js** application: an offline-first app where all data lives in
12
12
  2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
13
  3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
14
  4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
- 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls put errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
16
  6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
17
  7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
18
 
@@ -52,12 +52,12 @@ export default defineSchema({
52
52
  })
53
53
  ```
54
54
 
55
- If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
55
+ If you change a collection's shape, increment `version` and add a `migrations` entry for it (`migrate().addField(...)`): each device migrates its local database on its next open. `npx kora migrate` generates the matching migration for server databases and a transform stub for clients still on the older version.
56
56
 
57
57
  ## Project conventions
58
58
 
59
59
  - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
- - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, arrays as multisets that keep every device's additions and removals, objects per top-level key). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
61
  - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
62
 
63
63
  ## Commands
@@ -81,4 +81,4 @@ const rows = useQuery(orderedTodos(todos)) // readonly reactive ref
81
81
  const addTodo = useMutation((data) => todos.insert(data))
82
82
  ```
83
83
 
84
- Rules: always surface the mutation's error state in the UI. `useQuery` reads synchronously from the local store, so avoid loading spinners for local data. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
84
+ Rules: always surface the mutation's error state in the UI. `useQuery` reads the local store (no network, no spinner); the ref holds `[]` until the first local result. The Kora context is provided by the `KoraProvider` component wired in `src/main.ts`.
@@ -3,7 +3,9 @@ import {
3
3
  type CreateKoraAuthServerOptions,
4
4
  createKoraAuthServer,
5
5
  createPostgresOAuthStores,
6
+ createPostgresUserStore,
6
7
  createSqliteOAuthStores,
8
+ createSqliteUserStore,
7
9
  googleProvider,
8
10
  } from '@korajs/auth/server'
9
11
  import {
@@ -101,8 +103,14 @@ async function createAuth() {
101
103
  }
102
104
 
103
105
  const oauth = await createOAuthConfig()
106
+ // Users and token revocations persist in the same database (the user store's
107
+ // revocation store is used automatically), so sign-outs survive restarts.
108
+ const userStore = process.env.DATABASE_URL
109
+ ? await createPostgresUserStore({ connectionString: process.env.DATABASE_URL })
110
+ : await createSqliteUserStore({ filename: process.env.KORA_AUTH_DB || './.kora/kora-auth.db' })
104
111
  return createKoraAuthServer({
105
112
  jwtSecret: process.env.KORA_AUTH_SECRET,
113
+ userStore,
106
114
  ...(oauth ? { oauth } : {}),
107
115
  })
108
116
  }
@@ -1,5 +1,6 @@
1
1
  import { copyFileSync, existsSync, readdirSync } from 'node:fs'
2
2
  import { join, resolve } from 'node:path'
3
+ import { koraServiceWorker } from '@korajs/cli/vite'
3
4
  import tailwindcss from '@tailwindcss/vite'
4
5
  import vue from '@vitejs/plugin-vue'
5
6
  import type { Plugin } from 'vite'
@@ -57,8 +58,10 @@ function sqliteWasmHotfix(): Plugin {
57
58
  }
58
59
  }
59
60
 
61
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
62
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
60
63
  export default defineConfig({
61
- plugins: [vue(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
64
+ plugins: [vue(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
62
65
  worker: {
63
66
  format: 'es',
64
67
  },