@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
@@ -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,11 +77,11 @@ 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".
@@ -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,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 {
@@ -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 react from '@vitejs/plugin-react'
4
5
  import type { Plugin } from 'vite'
5
6
  import { defineConfig } from 'vite'
@@ -70,8 +71,10 @@ function sqliteWasmHotfix(): Plugin {
70
71
  }
71
72
  }
72
73
 
74
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
75
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
73
76
  export default defineConfig({
74
- plugins: [react(), crossOriginIsolation(), sqliteWasmHotfix()],
77
+ plugins: [react(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
75
78
  worker: {
76
79
  format: 'es',
77
80
  },
@@ -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,11 +77,11 @@ 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".
@@ -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 react from '@vitejs/plugin-react'
5
6
  import type { Plugin } from 'vite'
@@ -71,8 +72,16 @@ function sqliteWasmHotfix(): Plugin {
71
72
  }
72
73
  }
73
74
 
75
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
76
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
74
77
  export default defineConfig({
75
- plugins: [react(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
78
+ plugins: [
79
+ react(),
80
+ tailwindcss(),
81
+ crossOriginIsolation(),
82
+ sqliteWasmHotfix(),
83
+ koraServiceWorker(),
84
+ ],
76
85
  worker: {
77
86
  format: 'es',
78
87
  },
@@ -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
@@ -69,19 +69,21 @@ If you bump collections in a way that changes shapes, increment `version` and ru
69
69
 
70
70
  ## Sync and auth
71
71
 
72
- Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })`. In dev, the sync URL is derived from the page host and proxied by Vite; in production set `VITE_SYNC_URL`. Auth (if present in this template) uses `@korajs/auth`; the client is created in `src/auth.ts`. Local writes work without sign-in; sync requires the server to accept the connection.
72
+ Sync is configured in `src/kora.ts` via `createApp({ sync: { url, authClient } })`. In dev, the sync URL is derived from the page host and proxied by Vite; in production set `VITE_SYNC_URL`. Auth (if present in this template) uses `@korajs/auth`; the client is created in `src/auth.ts`. Local writes work without sign-in; sync requires the server to accept the connection.
73
73
 
74
74
  ## React bindings
75
75
 
76
+ Import the hooks from `src/kora.ts`, where `createKoraHooks<typeof app>()` binds them to this app's schema: collection names, inserts, updates, `where` and the rows `useQuery` returns are type-checked. Do not import them from `@korajs/react` directly (those are untyped).
77
+
76
78
  ```tsx
77
- import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
79
+ import { useCollection, useMutation, useQuery, useSyncStatus } from './kora'
78
80
 
79
81
  const todos = useCollection('todos')
80
- const rows = useQuery(orderedTodos(todos)) // reactive, no loading state for local data
82
+ const rows = useQuery(orderedTodos(todos)) // reactive; [] on the very first render
81
83
  const addTodo = useMutation((data) => todos.insert(data))
82
84
  addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
83
85
  await addTodo.mutateAsync({ title: 'x' }) // throws on failure; use when you need the error
84
86
  const status = useSyncStatus() // status.status, status.pendingOperations
85
87
  ```
86
88
 
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.
89
+ 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".
@@ -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,4 @@
1
1
  import { useAuth } from '@korajs/auth/react'
2
- import { useSyncStatus } from '@korajs/react'
3
2
  import {
4
3
  AlertCircle,
5
4
  CheckCircle2,
@@ -12,6 +11,7 @@ import {
12
11
  WifiOff,
13
12
  } from 'lucide-react'
14
13
  import { useState } from 'react'
14
+ import { useSyncStatus } from './kora'
15
15
  import { useTodos } from './modules/todos/useTodos'
16
16
 
17
17
  type Filter = 'all' | 'active' | 'completed'
@@ -0,0 +1,32 @@
1
+ import { createKoraAuthSync } from '@korajs/auth'
2
+ import { createApp } from 'korajs'
3
+ import { createKoraHooks } from 'korajs/react'
4
+ import { authClient } from './auth'
5
+ import koraWorkerUrl from './kora-worker.ts?worker&url'
6
+ import schema from './schema'
7
+
8
+ // Build sync URL: use env var if set, otherwise derive from current page host.
9
+ // This allows the Vite proxy (/kora-sync → ws://localhost:3001) to work in dev,
10
+ // and also works through any tunnel (ngrok, cloudflared) without extra configuration.
11
+ const syncUrl =
12
+ import.meta.env.VITE_SYNC_URL ||
13
+ `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/kora-sync`
14
+
15
+ export const app = createApp({
16
+ schema,
17
+ sync: {
18
+ url: syncUrl,
19
+ authClient: createKoraAuthSync({ authClient, schema }),
20
+ },
21
+ store: {
22
+ workerUrl: koraWorkerUrl,
23
+ },
24
+ devtools: import.meta.env.DEV,
25
+ })
26
+
27
+ /** The typed `todos` collection: inserts, updates and queries are checked against the schema. */
28
+ export type Todos = typeof app.todos
29
+
30
+ // Hooks bound to this app's schema: `useCollection('todoz')` is a type error, and the
31
+ // rows `useQuery` returns are typed from the query.
32
+ export const { useCollection, useQuery, useMutation, useSyncStatus } = createKoraHooks<typeof app>()
@@ -1,33 +1,11 @@
1
- import { createKoraAuthSync } from '@korajs/auth'
2
1
  import { AuthProvider } from '@korajs/auth/react'
3
2
  import { KoraProvider } from '@korajs/react'
4
- import { createApp } from 'korajs'
5
3
  import { StrictMode } from 'react'
6
4
  import { createRoot } from 'react-dom/client'
7
5
  import { App } from './App'
8
6
  import { authClient, completeOAuthCallbackFromLocation } from './auth'
9
- import schema from './schema'
7
+ import { app } from './kora'
10
8
  import './index.css'
11
- import koraWorkerUrl from './kora-worker.ts?worker&url'
12
-
13
- // Build sync URL: use env var if set, otherwise derive from current page host.
14
- // This allows the Vite proxy (/kora-sync → ws://localhost:3001) to work in dev,
15
- // and also works through any tunnel (ngrok, cloudflared) without extra configuration.
16
- const syncUrl =
17
- import.meta.env.VITE_SYNC_URL ||
18
- `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/kora-sync`
19
-
20
- const app = createApp({
21
- schema,
22
- sync: {
23
- url: syncUrl,
24
- authClient: createKoraAuthSync({ authClient, schema }),
25
- },
26
- store: {
27
- workerUrl: koraWorkerUrl,
28
- },
29
- devtools: import.meta.env.DEV,
30
- })
31
9
 
32
10
  // Connect to sync server once the app is ready
33
11
  app.ready.then(() => app.sync?.connect())
@@ -1,4 +1,4 @@
1
- import type { CollectionAccessor } from 'korajs'
1
+ import type { Todos } from '../../kora'
2
2
 
3
3
  export interface CreateTodoInput {
4
4
  title: string
@@ -8,18 +8,14 @@ export interface UpdateTodoStatusInput {
8
8
  completed: boolean
9
9
  }
10
10
 
11
- export function createTodo(todos: CollectionAccessor, data: CreateTodoInput) {
11
+ export function createTodo(todos: Todos, data: CreateTodoInput) {
12
12
  return todos.insert({ title: data.title })
13
13
  }
14
14
 
15
- export function updateTodoStatus(
16
- todos: CollectionAccessor,
17
- id: string,
18
- data: UpdateTodoStatusInput,
19
- ) {
15
+ export function updateTodoStatus(todos: Todos, id: string, data: UpdateTodoStatusInput) {
20
16
  return todos.update(id, { completed: data.completed })
21
17
  }
22
18
 
23
- export function deleteTodo(todos: CollectionAccessor, id: string) {
19
+ export function deleteTodo(todos: Todos, id: string) {
24
20
  return todos.delete(id)
25
21
  }
@@ -1,5 +1,5 @@
1
- import type { CollectionAccessor } from 'korajs'
1
+ import type { Todos } from '../../kora'
2
2
 
3
- export function orderedTodos(todos: CollectionAccessor) {
3
+ export function orderedTodos(todos: Todos) {
4
4
  return todos.where({}).orderBy('createdAt', 'desc')
5
5
  }
@@ -1,4 +1,4 @@
1
- import { useCollection, useMutation, useQuery } from '@korajs/react'
1
+ import { useCollection, useMutation, useQuery } from '../../kora'
2
2
  import {
3
3
  type CreateTodoInput,
4
4
  type UpdateTodoStatusInput,
@@ -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 {
@@ -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 react from '@vitejs/plugin-react'
5
6
  import type { Plugin } from 'vite'
@@ -71,8 +72,16 @@ function sqliteWasmHotfix(): Plugin {
71
72
  }
72
73
  }
73
74
 
75
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
76
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
74
77
  export default defineConfig({
75
- plugins: [react(), tailwindcss(), crossOriginIsolation(), sqliteWasmHotfix()],
78
+ plugins: [
79
+ react(),
80
+ tailwindcss(),
81
+ crossOriginIsolation(),
82
+ sqliteWasmHotfix(),
83
+ koraServiceWorker(),
84
+ ],
76
85
  worker: {
77
86
  format: 'es',
78
87
  },
@@ -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.
@@ -17,10 +17,16 @@
17
17
  "svelte": "^5.0.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
  "@sveltejs/vite-plugin-svelte": "^5.0.0",
23
28
  "svelte-check": "^4.1.0",
29
+ "tsx": "^4.19.0",
24
30
  "typescript": "^5.7.0",
25
31
  "vite": "^6.0.0"
26
32
  }
@@ -6,7 +6,8 @@ type Filter = 'all' | 'active' | 'completed'
6
6
  const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } =
7
7
  createTodosStores()
8
8
 
9
- const filter = $state<Filter>('all')
9
+ // biome-ignore lint/style/useConst: reassigned in the markup, which Biome does not see
10
+ let filter = $state<Filter>('all')
10
11
  let input = $state('')
11
12
 
12
13
  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 type { Plugin } from 'vite'
5
6
  import { defineConfig } from 'vite'
@@ -43,8 +44,10 @@ function sqliteWasmHotfix(): Plugin {
43
44
  }
44
45
  }
45
46
 
47
+ // koraServiceWorker() (build only, keep it last): precaches the built app shell so the
48
+ // app opens offline, with a "new version available" reload prompt. See @korajs/cli/vite.
46
49
  export default defineConfig({
47
- plugins: [svelte(), crossOriginIsolation(), sqliteWasmHotfix()],
50
+ plugins: [svelte(), crossOriginIsolation(), sqliteWasmHotfix(), koraServiceWorker()],
48
51
  worker: { format: 'es' },
49
52
  optimizeDeps: {
50
53
  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.
@@ -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
  }
@@ -10,7 +10,8 @@ const auth = createAuthStore()
10
10
  const todos = createTodosStores()
11
11
  const { allTodos, activeTodos, completedTodos, addTodo, toggleTodo, deleteTodo } = todos
12
12
 
13
- const filter = $state<Filter>('all')
13
+ // biome-ignore lint/style/useConst: reassigned in the markup, which Biome does not see
14
+ let filter = $state<Filter>('all')
14
15
  let input = $state('')
15
16
 
16
17
  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 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: [svelte(), crossOriginIsolation(), sqliteWasmHotfix()],
63
+ plugins: [svelte(), 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
@@ -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.