kreftjs 0.0.0-stage → 0.0.2-beta

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 (184) hide show
  1. package/README.md +103 -2
  2. package/bin/add.js +1074 -0
  3. package/bin/artisan-more.js +994 -0
  4. package/bin/artisan.js +1877 -0
  5. package/bin/backend/app.js +161 -0
  6. package/bin/backend/cache.js +196 -0
  7. package/bin/backend/collection.js +117 -0
  8. package/bin/backend/command.js +45 -0
  9. package/bin/backend/config.js +234 -0
  10. package/bin/backend/error-page.js +773 -0
  11. package/bin/backend/events.js +49 -0
  12. package/bin/backend/factory.js +223 -0
  13. package/bin/backend/fake.js +180 -0
  14. package/bin/backend/form.js +102 -0
  15. package/bin/backend/gate.js +79 -0
  16. package/bin/backend/gc.js +69 -0
  17. package/bin/backend/html.js +322 -0
  18. package/bin/backend/http.js +652 -0
  19. package/bin/backend/index.js +452 -0
  20. package/bin/backend/mail.js +77 -0
  21. package/bin/backend/mcp.js +433 -0
  22. package/bin/backend/migrator.js +370 -0
  23. package/bin/backend/model.js +781 -0
  24. package/bin/backend/namespace.js +213 -0
  25. package/bin/backend/notifications.js +68 -0
  26. package/bin/backend/qr.js +164 -0
  27. package/bin/backend/query.js +387 -0
  28. package/bin/backend/queue.js +412 -0
  29. package/bin/backend/redis.js +479 -0
  30. package/bin/backend/schedule.js +112 -0
  31. package/bin/backend/schema.js +321 -0
  32. package/bin/backend/seo-pages.js +100 -0
  33. package/bin/backend/storage.js +153 -0
  34. package/bin/backend/support.js +567 -0
  35. package/bin/backend/typescript.js +166 -0
  36. package/bin/build.js +540 -0
  37. package/bin/db-api.js +249 -0
  38. package/bin/db.js +275 -0
  39. package/bin/deps.js +138 -0
  40. package/bin/electron.js +509 -0
  41. package/bin/env.js +97 -0
  42. package/bin/guard.js +80 -0
  43. package/bin/kreftjs-config.js +40 -0
  44. package/bin/kreftjs.js +1373 -0
  45. package/bin/lang.js +241 -0
  46. package/bin/layout.js +74 -0
  47. package/bin/no-auth.js +162 -0
  48. package/bin/prerender.js +258 -0
  49. package/bin/safe-json.js +24 -0
  50. package/bin/server-routes.js +154 -0
  51. package/bin/ssr.js +133 -0
  52. package/bin/terminal.js +195 -0
  53. package/bin/to-ts.js +36 -0
  54. package/bin/update.js +237 -0
  55. package/bin/urls-file.js +27 -0
  56. package/bin/vite.js +228 -0
  57. package/docs/Docs.kreftjs +557 -0
  58. package/docs/README.md +2302 -0
  59. package/docs/markdown.js +186 -0
  60. package/editors/vscode/README.md +13 -0
  61. package/editors/vscode/extension.js +183 -0
  62. package/editors/vscode/language-configuration.json +17 -0
  63. package/editors/vscode/package.json +57 -0
  64. package/editors/vscode/snippets/kreftjs.json +21 -0
  65. package/editors/vscode/syntaxes/kreftjs.tmLanguage.json +207 -0
  66. package/kreftjs.d.ts +579 -0
  67. package/kreftjs.js +5108 -0
  68. package/package.json +97 -4
  69. package/server.d.ts +1028 -0
  70. package/server.js +8 -0
  71. package/templates/app/README.md +127 -0
  72. package/templates/app/_env.example +64 -0
  73. package/templates/app/_gitignore +5 -0
  74. package/templates/app/_vscode/settings.json +9 -0
  75. package/templates/app/backend/app/Auth/Session.js +177 -0
  76. package/templates/app/backend/app/Auth/Throttle.js +29 -0
  77. package/templates/app/backend/app/Console/Commands/.gitkeep +0 -0
  78. package/templates/app/backend/app/Events/.gitkeep +0 -0
  79. package/templates/app/backend/app/Http/Controllers/Admin/DashboardController.js +34 -0
  80. package/templates/app/backend/app/Http/Controllers/Admin/UserController.js +53 -0
  81. package/templates/app/backend/app/Http/Controllers/Auth/ApiTokenController.js +47 -0
  82. package/templates/app/backend/app/Http/Controllers/Auth/AuthenticatedSessionController.js +41 -0
  83. package/templates/app/backend/app/Http/Controllers/Auth/ConfirmablePasswordController.js +14 -0
  84. package/templates/app/backend/app/Http/Controllers/Auth/EmailVerificationController.js +23 -0
  85. package/templates/app/backend/app/Http/Controllers/Auth/NewPasswordController.js +29 -0
  86. package/templates/app/backend/app/Http/Controllers/Auth/PasswordResetLinkController.js +20 -0
  87. package/templates/app/backend/app/Http/Controllers/Auth/RegisteredUserController.js +19 -0
  88. package/templates/app/backend/app/Http/Controllers/Auth/SessionController.js +36 -0
  89. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorChallengeController.js +41 -0
  90. package/templates/app/backend/app/Http/Controllers/Auth/TwoFactorController.js +63 -0
  91. package/templates/app/backend/app/Http/Controllers/ProfileController.js +66 -0
  92. package/templates/app/backend/app/Http/Kernel.js +8 -0
  93. package/templates/app/backend/app/Http/Middleware/Authenticate.js +84 -0
  94. package/templates/app/backend/app/Http/Middleware/CheckAbilities.js +17 -0
  95. package/templates/app/backend/app/Http/Middleware/CheckForAnyAbility.js +12 -0
  96. package/templates/app/backend/app/Http/Middleware/EnsureEmailIsVerified.js +11 -0
  97. package/templates/app/backend/app/Http/Middleware/EnsureHasRole.js +11 -0
  98. package/templates/app/backend/app/Http/Middleware/EnsureIsAdmin.js +11 -0
  99. package/templates/app/backend/app/Http/Middleware/RequirePassword.js +15 -0
  100. package/templates/app/backend/app/Http/Requests/Auth/LoginRequest.js +45 -0
  101. package/templates/app/backend/app/Http/Resources/.gitkeep +1 -0
  102. package/templates/app/backend/app/Jobs/.gitkeep +0 -0
  103. package/templates/app/backend/app/Listeners/.gitkeep +0 -0
  104. package/templates/app/backend/app/Mail/.gitkeep +0 -0
  105. package/templates/app/backend/app/Models/PersonalAccessToken.js +21 -0
  106. package/templates/app/backend/app/Models/User.js +64 -0
  107. package/templates/app/backend/app/Notifications/.gitkeep +0 -0
  108. package/templates/app/backend/app/Observers/.gitkeep +0 -0
  109. package/templates/app/backend/app/Policies/.gitkeep +0 -0
  110. package/templates/app/backend/app/Providers/AppServiceProvider.js +12 -0
  111. package/templates/app/backend/app/Rules/.gitkeep +1 -0
  112. package/templates/app/backend/config/app.js +16 -0
  113. package/templates/app/backend/config/auth.js +34 -0
  114. package/templates/app/backend/config/database.js +38 -0
  115. package/templates/app/backend/config/redis.js +28 -0
  116. package/templates/app/backend/config/session.js +36 -0
  117. package/templates/app/backend/database/migrations/0001_01_01_000000_create_users_table.js +49 -0
  118. package/templates/app/backend/database/migrations/0001_01_01_000001_create_cache_table.js +13 -0
  119. package/templates/app/backend/database/migrations/0001_01_01_000002_create_jobs_table.js +26 -0
  120. package/templates/app/backend/database/migrations/0001_01_01_000003_create_notifications_table.js +18 -0
  121. package/templates/app/backend/database/seeders/DatabaseSeeder.js +15 -0
  122. package/templates/app/backend/lang/ar/messages.json +10 -0
  123. package/templates/app/backend/lang/ar/validation.json +21 -0
  124. package/templates/app/backend/lang/en/messages.json +10 -0
  125. package/templates/app/backend/lang/en/validation.json +19 -0
  126. package/templates/app/backend/routes/api.js +25 -0
  127. package/templates/app/backend/routes/auth.js +52 -0
  128. package/templates/app/backend/routes/console.js +15 -0
  129. package/templates/app/frontend/App.kreftjs +4 -0
  130. package/templates/app/frontend/components/Field.kreftjs +19 -0
  131. package/templates/app/frontend/components/StatCard.kreftjs +47 -0
  132. package/templates/app/frontend/components/profile/ApiTokens.kreftjs +70 -0
  133. package/templates/app/frontend/components/profile/Sessions.kreftjs +42 -0
  134. package/templates/app/frontend/components/profile/TwoFactor.kreftjs +90 -0
  135. package/templates/app/frontend/errors/ErrorMonitor.kreftjs +264 -0
  136. package/templates/app/frontend/index.html +79 -0
  137. package/templates/app/frontend/kreftjs.d.ts +579 -0
  138. package/templates/app/frontend/kreftjs.js +5108 -0
  139. package/templates/app/frontend/layouts/app.kreftjs +718 -0
  140. package/templates/app/frontend/layouts/docs.kreftjs +37 -0
  141. package/templates/app/frontend/layouts/guest.kreftjs +18 -0
  142. package/templates/app/frontend/pages/Dashboard.kreftjs +273 -0
  143. package/templates/app/frontend/pages/ErrorMonitor.kreftjs +96 -0
  144. package/templates/app/frontend/pages/Home.kreftjs +617 -0
  145. package/templates/app/frontend/pages/Profile.kreftjs +104 -0
  146. package/templates/app/frontend/pages/Users.kreftjs +240 -0
  147. package/templates/app/frontend/pages/auth/ConfirmPassword.kreftjs +33 -0
  148. package/templates/app/frontend/pages/auth/ForgotPassword.kreftjs +36 -0
  149. package/templates/app/frontend/pages/auth/Login.kreftjs +89 -0
  150. package/templates/app/frontend/pages/auth/Register.kreftjs +48 -0
  151. package/templates/app/frontend/pages/auth/ResetPassword.kreftjs +45 -0
  152. package/templates/app/frontend/pages/auth/VerifyEmail.kreftjs +59 -0
  153. package/templates/app/frontend/routes/web.kreftjs +34 -0
  154. package/templates/app/frontend/tests/components/Counter.kreftjs +15 -0
  155. package/templates/app/frontend/tests/pages/About.kreftjs +12 -0
  156. package/templates/app/frontend/tests/pages/Test.kreftjs +31 -0
  157. package/templates/app/frontend/ui.css +517 -0
  158. package/templates/app/lang/ar/auth.json +6 -0
  159. package/templates/app/lang/ar/messages.json +6 -0
  160. package/templates/app/lang/ar/pagination.json +5 -0
  161. package/templates/app/lang/ar/passwords.json +8 -0
  162. package/templates/app/lang/ar/validation.json +22 -0
  163. package/templates/app/lang/ar.json +10 -0
  164. package/templates/app/lang/en/auth.json +6 -0
  165. package/templates/app/lang/en/messages.json +6 -0
  166. package/templates/app/lang/en/pagination.json +5 -0
  167. package/templates/app/lang/en/passwords.json +8 -0
  168. package/templates/app/lang/en/validation.json +22 -0
  169. package/templates/app/lang/en.json +10 -0
  170. package/templates/errors/401.kreftjs +11 -0
  171. package/templates/errors/402.kreftjs +11 -0
  172. package/templates/errors/403.kreftjs +11 -0
  173. package/templates/errors/404.kreftjs +11 -0
  174. package/templates/errors/419.kreftjs +11 -0
  175. package/templates/errors/429.kreftjs +11 -0
  176. package/templates/errors/500.kreftjs +11 -0
  177. package/templates/errors/503.kreftjs +11 -0
  178. package/templates/errors/ErrorMonitor.kreftjs +502 -0
  179. package/templates/errors/error.kreftjs +11 -0
  180. package/templates/errors/minimal.kreftjs +25 -0
  181. package/tools/dev-client.js +125 -0
  182. package/tools/devtools.js +172 -0
  183. package/tools/vite-plugin-kreftjs.d.ts +52 -0
  184. package/tools/vite-plugin-kreftjs.js +466 -0
package/docs/README.md ADDED
@@ -0,0 +1,2302 @@
1
+ # Start here
2
+
3
+ ## Introduction
4
+
5
+ Kreft is a small reactive framework with its own component language. You write a component in a `.kreftjs` file: declare your state, derived values and actions in plain words, then describe the page in a `<view>`. The framework also ships a full Node.js backend (routes, controllers, models, migrations, queues, mail) so one project can cover the whole stack.
6
+
7
+ ```html
8
+ <!-- Counter.kreftjs -->
9
+ state count = 0
10
+ derive double = count * 2
11
+ action add() { count++ }
12
+
13
+ <view>
14
+ <button @click={add}>{count} × 2 = {double}</button>
15
+ </view>
16
+ ```
17
+
18
+ ### What you get
19
+
20
+ | Part | What it gives you |
21
+ |---|---|
22
+ | **The `.kreftjs` language** | `state`, `derive`, `action`, `watch`, props, events, slots, stores, a router with layouts and nested routes, forms with validation, animations. |
23
+ | **The backend** | `Route`, controllers, an Active Record `Model`, migrations, seeders, factories, jobs, mail, cache, file storage, policies, scheduling. |
24
+ | **Authentication** | Sign-in pages, password reset, email verification, two-factor codes, device sessions and API tokens, ready in every new project (leave them out with `--no-auth`). |
25
+ | **Tooling** | `kreftjs dev` with hot reload that keeps state, DevTools, an error monitor, `kreftjs build`, TypeScript, Vite and Electron support. |
26
+ | **Security by default** | Escaped views, CSRF protection, hashed passwords, rate limits, hardened package installs. See [Security overview](#security-overview). |
27
+
28
+ ### How the docs are organised
29
+
30
+ 1. **Start here** gets a project running.
31
+ 2. **The Kreft language** is everything in the browser.
32
+ 3. **Backend** is everything on the server.
33
+ 4. **Tooling** covers the command line, building and deploying.
34
+ 5. **Security** and **Reference** are for checking details.
35
+
36
+ > [!NOTE]
37
+ > Some chapters mention a few tools by name only (for example `kreftjs make:model`). Run `kreftjs list` to see every command that your installed version has.
38
+
39
+ ## Installation
40
+
41
+ ### Requirements
42
+
43
+ | You need | Why |
44
+ |---|---|
45
+ | [Node.js](https://nodejs.org) **22.5 or newer** (npm comes with it) | Runs the `kreftjs` command, the dev server and the backend. 22.5 also gives you SQLite with nothing to install. |
46
+ | A terminal | macOS Terminal, Windows PowerShell, or any Linux shell. |
47
+ | A modern browser | Chrome, Edge, Firefox or Safari. |
48
+ | MySQL, PostgreSQL or Redis | Only if you choose them. SQLite needs nothing. |
49
+
50
+ Check your Node version:
51
+
52
+ ```bash
53
+ node --version # v22.5.0 or higher
54
+ ```
55
+
56
+ ### Install the command
57
+
58
+ Pick one way.
59
+
60
+ **1. Once, for every project (recommended).** The `kreftjs` command then works in any folder:
61
+
62
+ ```bash
63
+ npm i -g kreftjs
64
+ ```
65
+
66
+ **2. Without installing anything.** `npx` downloads Kreft, runs it and forgets it. Good for trying it out:
67
+
68
+ ```bash
69
+ npx kreftjs new my-app
70
+ ```
71
+
72
+ **3. Inside one project.** The command is saved in the project's `package.json`, so everyone who clones the project gets the same version, and so does your server or CI:
73
+
74
+ ```bash
75
+ npm i -D kreftjs
76
+ npx kreftjs dev
77
+ ```
78
+
79
+ > [!NOTE]
80
+ > The scripts of a new project (`npm run dev`, `build`, `serve` …) call `kreftjs`. With way 1 that is the global command. With way 3, npm finds it in the project's `node_modules` by itself. On a server where you did not install it globally, run `npm i -D kreftjs` in the project before `npm run build` or `npm run serve`.
81
+
82
+ ### Check that it works
83
+
84
+ ```bash
85
+ kreftjs --version # for example 0.0.2-beta
86
+ kreftjs about # Kreft and Node.js versions, this project's settings and drivers
87
+ kreftjs list # every command your version has
88
+ ```
89
+
90
+ `kreft` is a shorter name for the same command.
91
+
92
+ ### Set up your editor
93
+
94
+ ```bash
95
+ kreftjs vscode
96
+ ```
97
+
98
+ installs colors and snippets for `.kreftjs` files in VS Code (also Cursor and VSCodium). Restart the editor afterwards.
99
+
100
+ ### Update
101
+
102
+ ```bash
103
+ npm i -g kreftjs@latest # the command (or: npm i -D kreftjs@latest inside a project)
104
+ cd my-app
105
+ kreftjs update --check # what would change
106
+ kreftjs update --core # refresh this app's kreftjs.js and type files (a backup is saved)
107
+ ```
108
+
109
+ Every app has its own copy of the browser runtime, `frontend/kreftjs.js`. Updating the command does not touch it, so an existing app keeps its old version until you run `kreftjs update --core` inside it.
110
+
111
+ ### Uninstall
112
+
113
+ ```bash
114
+ npm rm -g kreftjs
115
+ ```
116
+
117
+ ### Working on Kreft itself
118
+
119
+ Run `npm link` inside the Kreft folder. The `kreftjs` command then runs that folder's code, and every edit shows at once. Undo it with `npm unlink -g kreftjs`.
120
+
121
+ ### If something goes wrong
122
+
123
+ | What you see | What to do |
124
+ |---|---|
125
+ | `command not found: kreftjs` | npm's global folder is not on your `PATH`. Run `npm config get prefix`, add its `bin` folder to `PATH`, and open a new terminal. |
126
+ | `EACCES: permission denied` while installing | Do not use `sudo`. Install Node with [nvm](https://github.com/nvm-sh/nvm), or run `npm config set prefix ~/.npm-global` and add `~/.npm-global/bin` to `PATH`. |
127
+ | `SQLite needs Node.js 22.5 or newer` | Update Node.js. `node --version` must show 22.5.0 or higher. |
128
+ | `kreftjs --version` shows an old number | Another copy comes first on your `PATH`. Run `which -a kreftjs` (Windows: `where kreftjs`) and remove the old one. |
129
+ | PowerShell says "running scripts is disabled" (Windows) | Run `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned`, or use Command Prompt. |
130
+ | The browser shows a blank page or "Not found: /" | Run `kreftjs dev` from the project's top folder (the one with `frontend/` and `backend/`). The dev server serves `frontend/`. |
131
+
132
+ Next: [Your first app](#your-first-app).
133
+
134
+ ### Migrating from Som.js
135
+
136
+ Kreft is Som.js under a new name (the package and command are `kreftjs`), and an existing project keeps working without edits. `kreft`, an earlier spelling of the new name, is accepted in the same places:
137
+
138
+ - **Files:** `.som` (and `.kreft`) files still compile, next to `.kreftjs` ones. A name that is missing with one extension is tried with the others. Everything the tools write is `.kreftjs`.
139
+ - **Templates:** `s-if`, `s-each` and every other `s-` directive are read as `k-if`, `k-each` …, and `<som-view>`, `<som-link>` … as `<kreft-view>`, `<kreft-link>` …
140
+ - **JavaScript:** `Som` and `Kreft` are other names for `Kreftjs` (same object; `Som` is deprecated), and `Som.compileSom` is `Kreftjs.compile`.
141
+ - **Command:** `som` and `kreft` still run `kreftjs`; `som` prints a one-line notice.
142
+ - **Server code:** `require('somjs/server')` (also `som/server`, `@somjs/server`, `kreft/server`) still works; new code uses `require('kreftjs/server')`.
143
+ - **Settings:** `kreft.config.json`, `som.config.json` and `SOM_` environment variables (`SOM_SSR`, `SOM_ALLOWED_HOSTS` …) are read when the `kreftjs.config.json` file or the `KREFT_` variable is missing.
144
+ - **Cookies:** new projects use `kreft_session`, `kreft_2fa` and `kreft_bypass`. An existing project keeps its `som_` names until you change `app/Auth/Session.js`, and the framework's CSRF check accepts both.
145
+ - **Kept on purpose:** the `som_migrations` table, the `som:` prefix of saved data in `localStorage`, and the `som:` Redis keys of the queue. Renaming them needs a migration, planned for 3.1.
146
+
147
+ To move a project over, rename `*.som` (or `*.kreft`) to `*.kreftjs` (and the paths that mention them), run `kreftjs update` and replace `s-` with `k-`.
148
+
149
+ ## Your first app
150
+
151
+ Create a project, move into it and start the development server:
152
+
153
+ ```bash
154
+ kreftjs new my-app # asks: JavaScript or TypeScript?
155
+ cd my-app
156
+ npm run dev
157
+ ```
158
+
159
+ Pass `--js` or `--ts` to skip the question, and `--db sqlite` or `--ui tailwind` (also `bootstrap`, `bulma`, `pico`) to start with a database or a CSS library.
160
+
161
+ A new app comes with login, registration, password reset, email verification, a profile and an admin area. It asks whether you want them; pass `--auth` or `--no-auth` to skip the question. `--no-auth` starts a plain site: the welcome page, the docs, the error monitor and the tests (and the Todos example with `--db`), with no users table, sessions or sign-in pages.
162
+
163
+ Open the address the terminal shows (normally `http://localhost:8000`). If that port is busy, the next free one is used.
164
+
165
+ The new project already has a working login page. `npm run dev` creates the tables and a test user, **test@example.com** with the password **password**. Emails (verify, reset) are printed in the terminal until you set `MAIL_URL`.
166
+
167
+ > [!WARNING]
168
+ > The test user is for local development only. Delete it, or never run `--seed` against a real database.
169
+
170
+ ### The smallest possible app
171
+
172
+ You can use Kreft without any project at all. An app is an HTML page plus `.kreftjs` files:
173
+
174
+ ```html
175
+ <!-- index.html -->
176
+ <div id="app"></div>
177
+ <script src="kreftjs.js"></script>
178
+ <script>
179
+ Kreftjs.app('App.kreftjs').mount('#app')
180
+ </script>
181
+ ```
182
+
183
+ ```html
184
+ <!-- App.kreftjs -->
185
+ state name = 'World'
186
+
187
+ <view>
188
+ <h1>Hello {name}</h1>
189
+ <input k-model="name">
190
+ </view>
191
+ ```
192
+
193
+ > [!WARNING]
194
+ > `.kreftjs` files need a web server. Browsers refuse to load files from `file://`, so always open your app through `npm run dev` or any other server.
195
+
196
+ ## Project structure
197
+
198
+ `kreftjs new` creates two folders that never mix: `frontend/` is what browsers receive, `backend/` is the server and is never reachable over HTTP.
199
+
200
+ ```text
201
+ my-app/
202
+ ├── frontend/
203
+ │ ├── index.html entry point
204
+ │ ├── App.kreftjs root component and router container
205
+ │ ├── routes/web.kreftjs page routes
206
+ │ ├── layouts/ app.kreftjs (signed in), guest.kreftjs (sign-in pages)
207
+ │ ├── pages/ one .kreftjs file per screen, auth/ for sign-in pages
208
+ │ ├── components/ reusable pieces
209
+ │ ├── ui.css the design system
210
+ │ └── tests/ component tests
211
+ ├── backend/
212
+ │ ├── config/ app.js, auth.js, database.js, redis.js, session.js
213
+ │ ├── app/
214
+ │ │ ├── Http/ Controllers, Middleware, Requests, Resources
215
+ │ │ ├── Models/ User, PersonalAccessToken
216
+ │ │ └── Auth/ session helpers
217
+ │ ├── database/ migrations, seeders, factories
218
+ │ └── routes/ api.js, auth.js, console.js
219
+ ├── .env secrets and settings (never commit it)
220
+ ├── package.json
221
+ └── kreftjs.config.json CLI options
222
+ ```
223
+
224
+ ### Why the two folders matter
225
+
226
+ - `kreftjs dev` and `kreftjs build` serve only `frontend/`. The `backend/` folder, database files and `.env` have no web address.
227
+ - On macOS and Linux `kreftjs new` gives `backend/` and `.env` permissions that only the owner can read.
228
+ - Files such as `.env`, `*.db`, `*.pem`, `package.json` and `kreftjs.config.json` are refused even if requested by name.
229
+
230
+ ### Adding a CSS library
231
+
232
+ Any CSS library works. Add its `<link>` to `index.html`, or install it and import the file from a `.kreftjs` file; it becomes a `<link>` added once, and `kreftjs build` copies it (with its fonts and images) into `dist/vendor/`.
233
+
234
+ ```html
235
+ import 'bootstrap/dist/css/bootstrap.min.css' // npm i bootstrap
236
+ import './theme.css' // your own file, next to this .kreftjs file
237
+ ```
238
+
239
+ ### npm packages in the browser
240
+
241
+ Install a package with `kreftjs add`, then import it in any `.kreftjs` file:
242
+
243
+ ```bash
244
+ kreftjs add dayjs nanoid lodash
245
+ kreftjs add -D esbuild # once per project: it prepares packages for the browser
246
+ ```
247
+
248
+ ```html
249
+ import dayjs from 'dayjs'
250
+ import { nanoid } from 'nanoid'
251
+
252
+ state id = nanoid()
253
+
254
+ <view>
255
+ <p>{dayjs().format('YYYY-MM-DD')}</p>
256
+ </view>
257
+ ```
258
+
259
+ `kreftjs dev` serves each package as one browser-ready file and tells the browser where it is with an import map. Old-style packages (`module.exports`, such as lodash) work too.
260
+
261
+ # The Kreft language
262
+
263
+ ## Components and the .kreftjs file
264
+
265
+ A `.kreftjs` file has three parts:
266
+
267
+ 1. **The script**: everything outside `<view>` and `<style>`. Each line starts with a keyword.
268
+ 2. **`<view>`**: the HTML of the component.
269
+ 3. **`<style>`** (optional): CSS. Add `scoped` so it applies only to this component.
270
+
271
+ ### Keywords
272
+
273
+ | Keyword | Example | Meaning |
274
+ |---|---|---|
275
+ | `state` | `state count = 0` | A value that can change. When it changes, the view updates. |
276
+ | `derive` | `derive double = count * 2` | A value calculated from other values. It updates by itself. |
277
+ | `action` | `action add(n) { count += n }` | A function you call from the view or from other actions. |
278
+ | `when` | `when count > 10 { … }` | Runs the block each time the condition becomes true. |
279
+ | `watch` | `watch count { … }` | Runs each time the value changes. Inside, `value` is the new value and `old` the previous one. |
280
+ | `on` | `on ready { … }` | Lifecycle: `on start`, `on ready`, `on update`, `on show`, `on hide`, `on destroy`. |
281
+ | `prop` | `prop start = 0` | A value the parent passes in. The value after `=` is the default. |
282
+ | `event` | `event save, remove` | Events this component sends to its parent with `send('save', data)`. |
283
+ | `use` | `use cart` | Use a shared store. `use mouse = trackMouse()` uses shared logic from a `.js` file. |
284
+ | `share` / `receive` | `share theme = 'dark'` | Give a value to everything inside this component / take a value a parent shared. |
285
+ | `import` | `import Card from './Card.kreftjs'` | Use another component or a JavaScript module. |
286
+ | `saved` | `saved todos = []` | Like `state`, but kept in the browser (localStorage). |
287
+ | `load` | `load users = api.get('/api/users')` | Loads data and gives `users.data`, `users.loading`, `users.error`, `users.reload()`. |
288
+
289
+ Inside the script you use names **directly**: write `count++`, not `this.count++`.
290
+
291
+ ### A complete example
292
+
293
+ ```html
294
+ <!-- TodoPage.kreftjs -->
295
+ state todos = []
296
+ state text = ''
297
+
298
+ derive remaining = todos.filter((t) => !t.done).length
299
+
300
+ action add() {
301
+ if (!text.trim()) return
302
+ todos.push({ id: Date.now(), text, done: false })
303
+ text = ''
304
+ }
305
+
306
+ action clearDone() {
307
+ todos = todos.filter((t) => !t.done)
308
+ }
309
+
310
+ when todos.length && remaining === 0 {
311
+ bus.send('toast', 'All done!')
312
+ }
313
+
314
+ <view>
315
+ <form @submit.prevent={add}>
316
+ <input k-model="text" placeholder="New todo">
317
+ </form>
318
+ <label k-each="todo in todos" k-key="todo.id">
319
+ <input type="checkbox" k-model="todo.done"> {todo.text}
320
+ </label>
321
+ <p>{remaining} left · <button @click={clearDone}>Clear done</button></p>
322
+ </view>
323
+ ```
324
+
325
+ ### Good to know
326
+
327
+ - An `action` that uses `await` becomes async automatically.
328
+ - A line that does not start with a keyword is normal JavaScript (`let timer`, `const max = 10`). Your actions can use it, but the view cannot. Use `state`, `derive` or `action` for anything the view needs.
329
+ - Every Kreft function is available in the script: `bus`, `store`, `signal`, `watch`, `tick`, `send`, `props`, `$route`, `$router`.
330
+ - Errors name the file and line, for example `"state" needs a name, like: state count = 0 (Bad.kreftjs, line 3)`.
331
+
332
+ ### Lifecycle
333
+
334
+ | Hook | Runs |
335
+ |---|---|
336
+ | `on start` | Before the component is shown. Good for first data. |
337
+ | `on ready` | The component is on the screen. |
338
+ | `on update` | After the view changed. |
339
+ | `on show` / `on hide` | A [kept](#keep-and-wait) component comes back / is put away. |
340
+ | `on destroy` | The component is being removed. Clean up here. |
341
+
342
+ ## Templates and directives
343
+
344
+ ### Showing values
345
+
346
+ ```html
347
+ <h1>Hello {name}</h1>
348
+ <p>{count} items · total {total.toFixed(2)}</p>
349
+ <p>{isAdmin ? 'Admin' : 'User'}</p>
350
+ ```
351
+
352
+ Values are always escaped, so they are safe to show. To show a literal `{`, write `\{`.
353
+
354
+ ### Attributes
355
+
356
+ ```html
357
+ <img src={user.photo} alt={user.name}> <!-- the value as it is -->
358
+ <button disabled={count === 0}>Reset</button> <!-- false removes the attribute -->
359
+ <div class="card {active ? 'on' : ''}"> <!-- build a text -->
360
+ <a href="/users/{user.id}">Profile</a>
361
+ ```
362
+
363
+ `:class` and `:style` also accept objects: `:class="{ done: todo.done }"`.
364
+
365
+ ### Events
366
+
367
+ ```html
368
+ <button @click={add}>Add</button>
369
+ <button @click="add(5)">Add 5</button>
370
+ <input @input="search($event.target.value)">
371
+ <form @submit.prevent={save}>
372
+ <input @keyup.enter={send}>
373
+ ```
374
+
375
+ Modifiers: `.prevent` `.stop` `.self` `.once` `.capture` `.passive`. Keys: `.enter` `.esc` `.space` `.tab` `.up` `.down` `.ctrl` `.shift` `.alt` `.meta` `.exact`.
376
+
377
+ ### Directives
378
+
379
+ | Directive | What it does |
380
+ |---|---|
381
+ | `k-if` / `k-elif` / `k-else` | Adds or removes the element. |
382
+ | `k-each="item in list"` + `k-key="item.id"` | Repeats the element. Also `(item, i) in list`, `(value, key) in object`, `n in 10`. |
383
+ | `k-model="name"` | Two-way binding for input, textarea, select, checkbox and radio. Modifiers `.trim` `.number` `.lazy`. |
384
+ | `k-show` | Hides with CSS instead of removing. |
385
+ | `k-text` / `k-html` | Sets text, or HTML. `k-html` is cleaned of scripts and event handlers, but only bind HTML you trust. |
386
+ | `k-ref="box"` | Gives you the element: `state box = null` receives it. |
387
+ | `k-skip` | Leaves the element as it is (for code samples that contain `{ }`). |
388
+ | `k-once` | Draws the element once and never updates it. |
389
+ | `k-memo="[a, b]"` | Updates the element only when `a` or `b` changes. |
390
+ | `k-animate="fade"` | Animates the element when it comes and goes. |
391
+ | `<template k-if>` / `<template k-each>` | Groups elements without a wrapper. |
392
+ | `<kreft-component :is="name">` | Shows a component chosen while the app runs. |
393
+ | `<kreft-portal to="body">` | Renders content somewhere else (modals, toasts). |
394
+
395
+ ### Loops
396
+
397
+ ```html
398
+ <ul>
399
+ {for user in users key user.id}
400
+ <li>{user.name}</li>
401
+ {empty}
402
+ <li>No users yet</li>
403
+ {/for}
404
+ </ul>
405
+ ```
406
+
407
+ - `key user.id` is optional; it lets Kreft keep and move the right rows.
408
+ - `{empty}` is optional and shows when the list is empty.
409
+ - `{for (user, i) in users}` gives the index, `{for (value, key) in object}` walks an object, `{for n in 5}` counts 1 to 5.
410
+ - `k-each` on an element does the same: `<li k-each="user in users" k-key="user.id">`.
411
+
412
+ ### Animations
413
+
414
+ ```html
415
+ <p k-if="open" k-animate="fade">Hello</p>
416
+ <div k-show="menu" k-animate="slide">…</div>
417
+
418
+ <ul>
419
+ {for item in items key item.id animate slide}
420
+ <li>{item.name}</li>
421
+ {/for}
422
+ </ul>
423
+ ```
424
+
425
+ `k-animate` works with `k-if`, `k-show`, loops, `<kreft-component>` and `<kreft-view>`. Ready-made: `fade`, `slide`, `scale`, `pop`.
426
+
427
+ Your own animation is CSS with these classes (replace `name`):
428
+
429
+ | Class | When |
430
+ |---|---|
431
+ | `name-in` | The whole time it comes in (put the `transition` here). |
432
+ | `name-in-from` | The first moment: where it comes from. |
433
+ | `name-out` | The whole time it goes out. |
434
+ | `name-out-to` | Where it goes to. |
435
+ | `name-move` | A list item moving to its new place. |
436
+
437
+ ```css
438
+ .drop-in, .drop-out { transition: all .3s ease }
439
+ .drop-in-from { opacity: 0; transform: translateY(-30px) }
440
+ .drop-out-to { opacity: 0; transform: translateX(60px) }
441
+ ```
442
+
443
+ - Kreft waits for the longest `transition` or `animation` before removing the element.
444
+ - `k-animate.after` lets the old element leave before the new one comes in.
445
+ - Nothing animates when the page first opens; add `.first` to animate then too.
446
+
447
+ ### Faster views
448
+
449
+ Kreft already changes only the exact text or attribute that changed. For very large pages:
450
+
451
+ ```html
452
+ <footer k-once>Opened at {openedAt}</footer>
453
+
454
+ {for row in rows key row.id}
455
+ <tr k-memo="[row.id === selected]" class={row.id === selected ? 'on' : ''}>…</tr>
456
+ {/for}
457
+ ```
458
+
459
+ With `k-memo`, picking another row redraws two rows, not all of them.
460
+
461
+ ### Your own directives
462
+
463
+ ```js
464
+ app.directive('focus', {
465
+ ready(el, binding) { el.focus() },
466
+ update(el, binding) {},
467
+ destroy(el, binding) {},
468
+ })
469
+ ```
470
+
471
+ ```html
472
+ <input k-focus>
473
+ <div k-tooltip:top={message}></div> <!-- binding.arg = 'top', binding.value = message -->
474
+ ```
475
+
476
+ ## Props, events and slots
477
+
478
+ ### Using a component
479
+
480
+ Register it once by its path, or import it where you need it:
481
+
482
+ ```js
483
+ Kreftjs.app('App.kreftjs')
484
+ .component('UserCard', 'components/UserCard.kreftjs')
485
+ .mount('#app')
486
+ ```
487
+
488
+ ```html
489
+ import UserCard from './UserCard.kreftjs'
490
+
491
+ <view>
492
+ <user-card name="Alice Smith" age={30}></user-card>
493
+ </view>
494
+ ```
495
+
496
+ In the view, write component names in **kebab-case** (`<user-card>`).
497
+
498
+ ### Props: parent to child
499
+
500
+ ```html
501
+ <!-- UserCard.kreftjs -->
502
+ prop name
503
+ prop age = 0
504
+ prop admin = false
505
+
506
+ <view>
507
+ <div class="card">{name} ({age}) <b k-if="admin">admin</b></div>
508
+ </view>
509
+ ```
510
+
511
+ ```html
512
+ <user-card name="Alice Smith" age={30} admin></user-card>
513
+ ```
514
+
515
+ The default's type decides how plain text attributes are read: `age="30"` becomes the number `30`, and `admin` with no value becomes `true`. A child cannot change a prop; send an event instead.
516
+
517
+ ### Events: child to parent
518
+
519
+ ```html
520
+ <!-- TodoItem.kreftjs -->
521
+ prop todo
522
+ event remove
523
+
524
+ <view>
525
+ <li>{todo.text} <button @click="send('remove', todo.id)">✕</button></li>
526
+ </view>
527
+ ```
528
+
529
+ ```html
530
+ <!-- parent -->
531
+ action removeTodo(id) { todos = todos.filter((t) => t.id !== id) }
532
+
533
+ <view>
534
+ <todo-item k-each="t in todos" k-key="t.id" todo={t} @remove={removeTodo}></todo-item>
535
+ </view>
536
+ ```
537
+
538
+ ### `k-model` on a component
539
+
540
+ ```html
541
+ <!-- Toggle.kreftjs -->
542
+ prop modelValue = false
543
+ event update:modelValue
544
+
545
+ <view>
546
+ <button @click="send('update:modelValue', !modelValue)">{modelValue ? 'On' : 'Off'}</button>
547
+ </view>
548
+ ```
549
+
550
+ ```html
551
+ <toggle k-model="darkMode"></toggle>
552
+ ```
553
+
554
+ Named models: `k-model:title="pageTitle"` uses `prop title` and the event `update:title`.
555
+
556
+ ### Slots
557
+
558
+ ```html
559
+ <!-- Card.kreftjs -->
560
+ prop items = []
561
+
562
+ <view>
563
+ <div class="card">
564
+ <header><slot name="header">Default title</slot></header>
565
+ <slot></slot>
566
+ <li k-each="item in items"><slot name="item" item={item}>{item}</slot></li>
567
+ </div>
568
+ </view>
569
+ ```
570
+
571
+ ```html
572
+ <card items={users}>
573
+ <template k-slot:header>Users</template>
574
+ <p>Body content</p>
575
+ <template k-slot:item="{ item }"><b>{item.name}</b></template>
576
+ </card>
577
+ ```
578
+
579
+ ### Styles
580
+
581
+ - `<style>` applies to the whole page.
582
+ - `<style scoped>` applies only to this component.
583
+ - `.parent :deep(.child)` reaches into child components from scoped CSS.
584
+ - Attributes such as `class` that you put on a component tag land on its root element.
585
+
586
+ ### Keep and wait
587
+
588
+ **Keep** puts a component away instead of removing it. Typed text, scroll position and state are all still there when it returns:
589
+
590
+ ```html
591
+ <kreft-component :is="tab" keep></kreft-component>
592
+ <kreft-view keep></kreft-view>
593
+ <kreft-view keep="5"></kreft-view> <!-- keep at most 5; the one used longest ago goes -->
594
+ ```
595
+
596
+ ```html
597
+ on show { timer.start() } <!-- it came back (also the first time) -->
598
+ on hide { timer.stop() } <!-- it was put away -->
599
+ ```
600
+
601
+ **Wait** shows a placeholder until everything inside is ready (lazy components, every `load`, every `on start` that awaits):
602
+
603
+ ```html
604
+ <kreft-wait>
605
+ <user-profile id={5}></user-profile>
606
+ <p k-waiting>Loading…</p>
607
+ <p k-failed="error">Could not load: {error.message}</p>
608
+ </kreft-wait>
609
+ ```
610
+
611
+ ## Sharing state
612
+
613
+ | Need | Use |
614
+ |---|---|
615
+ | Parent to child | `prop` |
616
+ | Child to parent | `event` + `send()`, or `k-model` |
617
+ | Many components, anywhere | stores: `Kreftjs.store()` + `use` |
618
+ | A parent to everything inside it | `share` / `receive` |
619
+ | "Something happened" messages | `bus.send()` / `bus.on()` |
620
+
621
+ ### Stores
622
+
623
+ Create a store once in your page's script:
624
+
625
+ ```js
626
+ Kreftjs.store('cart', {
627
+ items: [],
628
+ get count() { return this.items.length },
629
+ get total() { return this.items.reduce((s, p) => s + p.price, 0) },
630
+ add(product) { this.items.push(product) },
631
+ })
632
+ ```
633
+
634
+ Use it in any `.kreftjs` file. Every component sees the same data:
635
+
636
+ ```html
637
+ use cart
638
+
639
+ <view>
640
+ <span>Cart: {cart.count} · {cart.total}</span>
641
+ <button @click="cart.add(product)">Add</button>
642
+ </view>
643
+ ```
644
+
645
+ Every store also has `$patch` (change several values at once), `$reset()` (back to the start) and `$watch(fn)` (runs on every change and stops by itself when the component goes away).
646
+
647
+ ### Shared logic
648
+
649
+ Logic that many components need goes in a `.js` file as a function. It can use `Kreftjs.state`, `Kreftjs.watch`, `Kreftjs.onDestroy` and the rest:
650
+
651
+ ```js
652
+ // logic/mouse.js
653
+ export function trackMouse() {
654
+ const pos = Kreftjs.state({ x: 0, y: 0 })
655
+ const move = (e) => { pos.x = e.clientX; pos.y = e.clientY }
656
+ window.addEventListener('pointermove', move)
657
+ Kreftjs.onDestroy(() => window.removeEventListener('pointermove', move))
658
+ return pos
659
+ }
660
+ ```
661
+
662
+ ```html
663
+ import { trackMouse } from '../logic/mouse.js'
664
+ use mouse = trackMouse()
665
+
666
+ <view><p>{mouse.x}, {mouse.y}</p></view>
667
+ ```
668
+
669
+ ### Share and receive
670
+
671
+ ```html
672
+ <!-- Layout.kreftjs -->
673
+ state settings = { theme: 'dark', lang: 'so' }
674
+ share settings = settings
675
+
676
+ <!-- any component inside it, however deep -->
677
+ receive settings
678
+
679
+ <view><p>Theme: {settings.theme}</p></view>
680
+ ```
681
+
682
+ Share a `state` object and every receiver updates when it changes. `Kreftjs.app(…).share('key', value)` shares with the whole app.
683
+
684
+ ### Event bus
685
+
686
+ ```html
687
+ <!-- sending -->
688
+ action save() { bus.send('toast', 'Saved!') }
689
+
690
+ <!-- listening -->
691
+ state message = ''
692
+ on ready { bus.on('toast', (text) => (message = text)) }
693
+ ```
694
+
695
+ Listeners added in `on start` or `on ready` are removed automatically when the component goes away. Others stay until you call `bus.off('toast', fn)`.
696
+
697
+ ## Saving data in the browser
698
+
699
+ Write `saved` instead of `state` and the value is kept in localStorage. It survives reloads and closing the browser.
700
+
701
+ ```html
702
+ saved todos = []
703
+ saved theme = 'light'
704
+ saved token = '' as 'auth.token' <!-- choose the storage key yourself -->
705
+ ```
706
+
707
+ - Every change is saved by itself, including deep ones: `todos.push(…)`, `todo.done = true`.
708
+ - The default key is `<Component>.<name>` (stored as `som:Home.todos`). Use `as '…'` to share one value between components.
709
+ - A change made in another tab of the same site shows up in this tab too.
710
+
711
+ A store keeps its plain values when you add `saved`:
712
+
713
+ ```js
714
+ Kreftjs.store('cart', { items: [] }, { saved: true }) // or { saved: ['items'] } for some fields
715
+ ```
716
+
717
+ From JavaScript:
718
+
719
+ ```js
720
+ const theme = Kreftjs.saved('theme', 'light') // a signal: theme.value = 'dark' is saved
721
+ Kreftjs.storage.set('user', { name: 'Alice Smith' })
722
+ Kreftjs.storage.get('user')
723
+ Kreftjs.storage.remove('user')
724
+ Kreftjs.storage.clear() // removes only what Kreft saved
725
+ ```
726
+
727
+ > [!WARNING]
728
+ > localStorage belongs to one browser on one site, holds about 5 MB and can be read by anyone using that browser, and by any script that runs on your page. Never store passwords or long-lived secrets in it.
729
+
730
+ ## Router and layouts
731
+
732
+ ### Defining routes
733
+
734
+ ```js
735
+ const router = Kreftjs.router({
736
+ routes: [
737
+ { path: '/', component: 'pages/Home.kreftjs', meta: { title: 'Home' } },
738
+ { path: '/users/:id', name: 'user', component: 'pages/User.kreftjs', props: true },
739
+ { path: '/posts/:slug?', component: 'pages/Posts.kreftjs' },
740
+ { path: '/old', redirect: '/' },
741
+ { path: '/admin', component: 'pages/Admin.kreftjs', guard: () => isAdmin() || '/login' },
742
+ { path: '*', component: 'pages/NotFound.kreftjs' },
743
+ ],
744
+ })
745
+
746
+ Kreftjs.app('App.kreftjs').use(router).mount('#app')
747
+ ```
748
+
749
+ ```html
750
+ <!-- App.kreftjs -->
751
+ <view>
752
+ <nav>
753
+ <kreft-link to="/">Home</kreft-link>
754
+ <kreft-link to="/users/{user.id}">Profile</kreft-link>
755
+ </nav>
756
+ <kreft-view></kreft-view>
757
+ </view>
758
+ ```
759
+
760
+ With `props: true`, the `:id` parameter arrives as a prop:
761
+
762
+ ```html
763
+ <!-- pages/User.kreftjs -->
764
+ prop id
765
+ derive user = users.find(id)
766
+ action back() { $router.back() }
767
+ ```
768
+
769
+ | Feature | How |
770
+ |---|---|
771
+ | Current route | `$route.path`, `$route.params.id`, `$route.query.tab`, `$route.meta` |
772
+ | Navigate | `$router.go('/users/5?tab=info')`, `$router.replace(to)`, `$router.back()` |
773
+ | Active link | The `kreft-link-active` and `kreft-link-exact` classes |
774
+ | Page title | `meta: { title: '…' }` |
775
+ | Check before every page | `router.before((to, from) => …)`. Return `false` to stop, or a path to go elsewhere. |
776
+ | Check one route | `guard: (to, from) => …` |
777
+ | After every page | `router.after((to, from) => …)` |
778
+
779
+ Each page file is loaded the first time someone opens it.
780
+
781
+ ### Route files
782
+
783
+ New projects keep their pages in `frontend/routes/web.kreftjs`, and `index.html` only points to it:
784
+
785
+ ```js
786
+ Kreftjs.router({
787
+ routes: ['routes/web.kreftjs', 'routes/shop.kreftjs'],
788
+ layout: 'app',
789
+ errors: 'errors',
790
+ middleware: {
791
+ auth: (to) => (auth.user ? true : '/login?redirect=' + encodeURIComponent(to.fullPath)),
792
+ guest: () => (auth.user ? '/dashboard' : true),
793
+ },
794
+ })
795
+ ```
796
+
797
+ ```js
798
+ // routes/web.kreftjs
799
+ Route.view('/', 'pages/Home.kreftjs').name('home').title('Home')
800
+ Route.view('/posts/:id', 'pages/Post.kreftjs')
801
+
802
+ Route.middleware('auth').group(() => {
803
+ Route.view('/dashboard', 'pages/Dashboard.kreftjs').middleware('verified')
804
+ })
805
+
806
+ Route.layout('guest').middleware('guest').group(() => {
807
+ Route.view('/login', 'pages/auth/Login.kreftjs').title('Log in')
808
+ })
809
+
810
+ Route.prefix('/account').name('account.').middleware('auth').group(() => {
811
+ Route.view('/settings', 'pages/account/Settings.kreftjs').children(() => {
812
+ Route.view('', 'pages/account/General.kreftjs') // /account/settings
813
+ Route.view('mail', 'pages/account/Mail.kreftjs') // /account/settings/mail
814
+ })
815
+ })
816
+
817
+ Route.redirect('/home', '/')
818
+ Route.fallback('pages/NotFound.kreftjs')
819
+ ```
820
+
821
+ | On a route | What it does |
822
+ |---|---|
823
+ | `.name('home')` | `$router.go({ name: 'home' })` |
824
+ | `.title('Home')` | The page title. |
825
+ | `.layout('guest')`, `.layout('none')` | Another layout, or none. |
826
+ | `.middleware('auth', 'role:editor')` | Runs these before the page. |
827
+ | `.meta({ … })` | Anything else, in `$route.meta`. |
828
+ | `.children(() => …)` | Nested pages, shown in the page's own `<kreft-view>`. |
829
+
830
+ > [!NOTE]
831
+ > Client-side `auth` middleware only decides which screens to show. It does not protect data: every API route must check the user on the server.
832
+
833
+ ### Layouts
834
+
835
+ A layout is the frame around your pages: bar, menu, footer. Put it in `layouts/` and use `<slot>` where the page goes.
836
+
837
+ ```html
838
+ <!-- layouts/app.kreftjs -->
839
+ state menuOpen = false
840
+
841
+ <view>
842
+ <header>…</header>
843
+ <main><slot></slot></main>
844
+ </view>
845
+ ```
846
+
847
+ | Where | What you write |
848
+ |---|---|
849
+ | Every page | `Kreftjs.router({ layout: 'app', routes })` |
850
+ | One route and its children | `{ path: '/admin', component: …, layout: 'admin' }` |
851
+ | Inside a page file | `layout admin` on its own line |
852
+ | No layout | `layout: false` on the route, or `layout none` in the page |
853
+
854
+ When more than one applies, the route wins, then the page file, then the router. Going between two pages that use the same layout keeps the layout on screen, with its state.
855
+
856
+ A page can also fill other places in its layout:
857
+
858
+ ```html
859
+ <!-- in the layout -->
860
+ <kreft-yield name="actions">shown when the page has no such section</kreft-yield>
861
+
862
+ <!-- in the page -->
863
+ <kreft-section name="actions">
864
+ <button class="primary" @click="save()">Save</button>
865
+ </kreft-section>
866
+ ```
867
+
868
+ ### Error pages
869
+
870
+ Each HTTP error can have its own page in `errors/`. Turn them on with `errors: 'errors'` in the router. New projects already have the folder (`kreftjs make:errors` adds it to an older one):
871
+
872
+ ```text
873
+ errors/401.kreftjs 402.kreftjs 403.kreftjs 404.kreftjs 419.kreftjs 429.kreftjs 500.kreftjs 503.kreftjs
874
+ errors/error.kreftjs any other status
875
+ errors/minimal.kreftjs the shared frame
876
+ ```
877
+
878
+ An unknown address shows `errors/404.kreftjs`; a page that cannot load shows `errors/500.kreftjs`. From any `.kreftjs` file, `abort(404)` (also `$router.abort()` and `Kreftjs.abort()`) shows the matching page:
879
+
880
+ ```html
881
+ prop id
882
+ load post = api.get('/api/posts/' + id)
883
+ watch post.error { if (post.error && post.error.status === 404) abort(404) }
884
+ ```
885
+
886
+ ### Clean URLs
887
+
888
+ By default the router uses URLs without `#` (`/users/5`). Two things make that work:
889
+
890
+ 1. A `<base href="/">` tag in `index.html` (use `<base href="/demo/">` if the app lives in a folder).
891
+ 2. The server must send `index.html` for every page path, so reloading `/users/5` is not a 404. `npm run dev` does this for you.
892
+
893
+ If you cannot change the server, use `mode: 'hash'`; URLs then look like `/#/users/5`.
894
+
895
+ ## Fetching data
896
+
897
+ The global `api` client is available in every `.kreftjs` file, and `Kreftjs.api` in plain JavaScript:
898
+
899
+ ```js
900
+ const users = await api.get('/api/users', { query: { page: 2 } })
901
+ const user = await api.post('/api/users', { name: 'Alice Smith' })
902
+ await api.put('/api/users/1', user)
903
+ await api.patch('/api/users/1', { status: 'active' })
904
+ await api.delete('/api/users/1')
905
+ ```
906
+
907
+ ### Loading data: `load`
908
+
909
+ `load` declares a reactive resource. It handles the loading state, the error, and fetches again when the values it reads change:
910
+
911
+ ```html
912
+ <!-- pages/Posts.kreftjs -->
913
+ state query = ''
914
+ state page = 1
915
+
916
+ load posts = api.get('/api/posts', { query: { q: query || undefined, page } })
917
+
918
+ <view>
919
+ <input type="search" k-model.lazy="query" placeholder="Search posts…">
920
+
921
+ <p k-if="posts.loading">Loading posts…</p>
922
+ <div k-elif="posts.error">
923
+ Failed to load: {posts.error.message}
924
+ <button @click={posts.reload}>Try again</button>
925
+ </div>
926
+ <div k-else>
927
+ {for post in posts.data || [] key post.id}
928
+ <article><h3>{post.title}</h3><p>{post.body}</p></article>
929
+ {empty}
930
+ <p>No posts found for "{query}".</p>
931
+ {/for}
932
+ </div>
933
+
934
+ <button @click={() => page++}>Next page ({page})</button>
935
+ </view>
936
+ ```
937
+
938
+ | Property | Meaning |
939
+ |---|---|
940
+ | `posts.data` | The result (`null` until loaded). |
941
+ | `posts.loading` | `true` while a request is running. |
942
+ | `posts.loaded` | `true` once the first answer arrived. |
943
+ | `posts.error` | The error, or `null`. |
944
+ | `posts.reload()` | Fetch again now. |
945
+
946
+ ### Tables
947
+
948
+ For tables served by `kreftjs dev --db` or the automatic table API, `table(name)` gives ready-made calls:
949
+
950
+ ```html
951
+ const posts = table('posts')
952
+
953
+ load list = posts.all({ q: search, _sort: '-created_at', _limit: 10 })
954
+
955
+ action createPost() {
956
+ const created = await posts.create({ title: 'New article' })
957
+ list.data = [created, ...list.data]
958
+ }
959
+ ```
960
+
961
+ Methods: `all(query)`, `find(id)`, `count(query)`, `create(data)`, `update(id, data)` (PUT), `patch(id, data)`, `remove(id)`.
962
+
963
+ ### Errors
964
+
965
+ A failed request throws an `ApiError` with `status`, `message`, `data` and `errors` (validation messages). Network problems and timeouts have `status` 0.
966
+
967
+ ```html
968
+ state errors = {}
969
+
970
+ action save() {
971
+ try {
972
+ await api.post('/api/users', form)
973
+ } catch (e) {
974
+ errors = e.errors || {} // { email: ['The email has already been taken.'] }
975
+ }
976
+ }
977
+ ```
978
+
979
+ ### Settings
980
+
981
+ ```js
982
+ Kreftjs.api.setup({
983
+ base: '/api', // then api.get('users') is /api/users
984
+ token: () => Kreftjs.storage.get('token'), // sent as Authorization: Bearer …
985
+ headers: { 'Accept-Language': 'en' },
986
+ timeout: 10000,
987
+ })
988
+
989
+ Kreftjs.api.onError((e) => { if (e.status === 401) location.href = '/login' })
990
+
991
+ const github = Kreftjs.api.create({ base: 'https://api.github.com' }) // a second client
992
+ ```
993
+
994
+ Options for one request: `query`, `headers`, `timeout`, `signal` (to cancel) and `full: true` (returns `{ data, status, headers }`).
995
+
996
+ Requests to your own backend also send `X-XSRF-TOKEN` from the `XSRF-TOKEN` cookie (and `X-CSRF-TOKEN` from `<meta name="csrf-token">`), plus `X-Requested-With`.
997
+
998
+ ### A practice API
999
+
1000
+ ```bash
1001
+ kreftjs dev --api db.json # a REST API at /api, made from a JSON file
1002
+ kreftjs dev --api db.json --delay 500 # answer slowly, to see loading states
1003
+ ```
1004
+
1005
+ It gives GET and POST on `/api/posts`, GET, PUT, PATCH and DELETE on `/api/posts/1`, search with `?q=text`, filters with `?field=value`, and standard paging and sorting.
1006
+
1007
+ > [!WARNING]
1008
+ > `--api` and `--db` are for development. They have no login. `kreftjs dev` answers only your own computer, and refuses requests that change data when they come from another web site, but do not expose them to the internet. In production, write real routes (see [Routing](#routing)).
1009
+
1010
+ ## Forms and validation
1011
+
1012
+ Give a form its rules in declarative style. A form with mistakes is never sent:
1013
+
1014
+ ```html
1015
+ state post = form({ title: '', email: '' }, { title: 'required|min:3|max:120', email: 'required|email' })
1016
+
1017
+ action save() {
1018
+ const created = await post.$submit((values) => api.post('/api/posts', values))
1019
+ if (created) post.$reset()
1020
+ }
1021
+
1022
+ <view>
1023
+ <form @submit.prevent={save}>
1024
+ <input k-model="post.title">
1025
+ <p k-if="post.$error('title')" class="error">{post.$error('title')}</p>
1026
+ <button disabled={post.$busy}>Save</button>
1027
+ </form>
1028
+ </view>
1029
+ ```
1030
+
1031
+ | On the form | What it is |
1032
+ |---|---|
1033
+ | `post.title`, `post.email` | The values, for `k-model`. |
1034
+ | `post.$error('title')` | The first message for a field, or `''`. |
1035
+ | `post.$errors` | Every message: `{ title: ['…'] }`. |
1036
+ | `post.$valid`, `post.$busy` | No mistakes / sending right now. |
1037
+ | `post.$submit(send)` | Checks, then calls `send(values)`. A 422 from the server fills the same errors. Gives `false` when it did not send. |
1038
+ | `post.$check()`, `post.$reset(values?)`, `post.$setErrors(e)` | Check now / start again / show errors from elsewhere. |
1039
+
1040
+ Errors appear after the first submit, then follow your typing.
1041
+
1042
+ Without a form, `validate(values, rules)` gives `{}` or `{ field: ['…'] }`. Custom messages go in the third argument: `form(values, rules, { messages: { 'title.required': 'Write a title.' } })`.
1043
+
1044
+ The same rule names work on the server in `req.validate()` (see [Controllers and requests](#controllers-and-requests)), so one set of rules can describe both sides.
1045
+
1046
+ ## Internationalization and RTL
1047
+
1048
+ Kreft translates text and flips direction for right-to-left languages such as Arabic, Hebrew, Persian and Urdu.
1049
+
1050
+ ### Translations
1051
+
1052
+ Put translation files in a `lang/` folder: `lang/` or `frontend/lang/` in your project (`backend/lang/` and `resources/lang/` are read too). Use `lang/en.json` for plain strings, or `lang/en/messages.json` for grouped keys. In any view:
1053
+
1054
+ ```html
1055
+ <h1>{$t('messages.welcome', { name: user.name })}</h1>
1056
+ <p>{$choice('messages.apples', count)}</p>
1057
+ ```
1058
+
1059
+ A message uses `:name` for values. For plurals, separate the forms with `|`: `{0} No apples|{1} One apple|[2,*] :count apples`. Under the hood, categories use the browser's `Intl.PluralRules` for the current locale.
1060
+
1061
+ Change the language with `Kreftjs.locale('ar')`. The page's `<html lang>` and `dir` update, and every view re-renders. `Kreftjs.fallbackLocale('en')` sets the fallback, `Kreftjs.locales()` lists loaded languages, and `Kreftjs.translations('so', { … })` registers more text from code.
1062
+
1063
+ ### Direction
1064
+
1065
+ `dir` is set for you from the locale, and you can also bind it by hand:
1066
+
1067
+ ```html
1068
+ <view dir={isArabic ? 'rtl' : 'ltr'}>…</view>
1069
+ ```
1070
+
1071
+ ### Write CSS that flips by itself
1072
+
1073
+ Use CSS logical properties instead of left/right:
1074
+
1075
+ | Avoid | Use |
1076
+ |---|---|
1077
+ | `margin-left` / `margin-right` | `margin-inline-start` / `margin-inline-end` |
1078
+ | `padding-left` / `padding-right` | `padding-inline-start` / `padding-inline-end` |
1079
+ | `left: 0` / `right: 0` | `inset-inline-start: 0` / `inset-inline-end: 0` |
1080
+ | `text-align: left` / `right` | `text-align: start` / `end` |
1081
+ | `border-left` / `border-right` | `border-inline-start` / `border-inline-end` |
1082
+
1083
+ ```css
1084
+ .sidebar {
1085
+ position: fixed;
1086
+ inset-inline-start: 0; /* left in LTR, right in RTL */
1087
+ padding-inline-start: 24px;
1088
+ border-inline-end: 1px solid var(--border);
1089
+ }
1090
+
1091
+ [dir="rtl"] .back-arrow { transform: scaleX(-1); } /* flip an icon */
1092
+ ```
1093
+
1094
+ On the server, request language comes from `?lang=`, the `lang` cookie or `Accept-Language`, and backend messages live in `backend/lang/<locale>/`.
1095
+
1096
+ ## Using Kreft from JavaScript
1097
+
1098
+ You can write components as plain objects. The rules match the `.kreftjs` language: values are state, getters are derived values, functions are actions.
1099
+
1100
+ ```js
1101
+ Kreftjs.app({
1102
+ count: 0,
1103
+ get double() { return this.count * 2 },
1104
+ add() { this.count++ },
1105
+ template: `<button @click={add}>{count} × 2 = {double}</button>`,
1106
+ }).mount('#app')
1107
+ ```
1108
+
1109
+ | JavaScript | Same as in `.kreftjs` |
1110
+ |---|---|
1111
+ | `Kreftjs.state({ … })` | `state` + `derive` + `action` together, for code outside components. |
1112
+ | `Kreftjs.signal(0)` | One value, read and written with `.value`. |
1113
+ | `Kreftjs.derived(() => …)` | `derive` |
1114
+ | `Kreftjs.watch(source, (value, old) => …)` | `watch` |
1115
+ | `Kreftjs.effect(() => …)` | Runs again whenever what it reads changes. |
1116
+ | `Kreftjs.tick()` | Wait until the page has updated. |
1117
+
1118
+ `Kreftjs.render(view, data)` turns a view and data into an HTML string, with every directive working and every value escaped. Use it for emails and print views:
1119
+
1120
+ ```js
1121
+ Kreftjs.render(`<ul><li k-each="u in users">{u}</li></ul>`, { users: ['Alex', 'Alice'] })
1122
+ // <ul><li>Alex</li><li>Alice</li></ul>
1123
+ ```
1124
+
1125
+ ### Server templates
1126
+
1127
+ Blade and similar engines use `{{ }}`; Kreft uses `{ }`, so they do not clash. Put `kreftjs.js` and your `.kreftjs` files in your public folder and mount as usual.
1128
+
1129
+ # Backend
1130
+
1131
+ ## Routing
1132
+
1133
+ A project made with `kreftjs new` has a Node.js backend. `kreftjs dev` and `kreftjs serve` load `backend/routes/api.js` by themselves, every address in it starts with `/api`, and `kreftjs dev` picks up each change you save. The backend folder never reaches the browser.
1134
+
1135
+ ```js
1136
+ // routes/api.js
1137
+ const { Route } = require('kreftjs/server')
1138
+ const PostController = require('../app/Http/Controllers/PostController')
1139
+
1140
+ Route.get('/hello', (req) => ({ hello: req.input('name', 'world') })) // GET /api/hello
1141
+
1142
+ Route.apiResource('posts', PostController) // index, store, show, update, destroy
1143
+ Route.get('/posts/{post}/comments', [PostController, 'comments']).name('posts.comments')
1144
+
1145
+ Route.middleware('auth').group(() => {
1146
+ Route.delete('/posts/{post}', [PostController, 'destroy'])
1147
+ })
1148
+
1149
+ Route.prefix('admin').middleware(['auth', 'admin']).group(() => { … })
1150
+ ```
1151
+
1152
+ - Parameters are written `{post}`, or `{post?}` when optional.
1153
+ - A route can have a name (`.name('…')`), a pattern (`.whereNumber('id')`) and middleware (`.middleware('auth')`).
1154
+ - `kreftjs route:list` prints every route with its name, controller and middleware.
1155
+
1156
+ ### Namespaces
1157
+
1158
+ PHP-style namespaces are supported for controllers, models and relations:
1159
+
1160
+ ```js
1161
+ const { Route, use, namespace } = require('kreftjs/server')
1162
+
1163
+ const User = use('App\\Models\\User')
1164
+ const { DashboardController, UserController } = namespace('App\\Http\\Controllers\\Admin')
1165
+
1166
+ Route.namespace('Admin').group(() => {
1167
+ Route.get('/users', 'UserController@index')
1168
+ Route.apiResource('users', 'UserController')
1169
+ })
1170
+ ```
1171
+
1172
+ ## Controllers and requests
1173
+
1174
+ A controller method gets the request first, then the route's parameters. What it returns is sent as JSON; returning nothing answers `204`.
1175
+
1176
+ ```js
1177
+ const { response, abort } = require('kreftjs/server')
1178
+ const Post = require('../../Models/Post')
1179
+
1180
+ class PostController {
1181
+ async index(req) {
1182
+ return Post.with('author').where('published', true).latest().paginate(15, req.integer('page', 1))
1183
+ }
1184
+
1185
+ async store(req) {
1186
+ const data = await req.validate({
1187
+ title: 'required|max:200|unique:posts,title',
1188
+ author_id: 'required|exists:authors,id',
1189
+ })
1190
+ return response(await Post.create(data), 201)
1191
+ }
1192
+
1193
+ async show(req, id) { return Post.findOrFail(id) } // 404 when missing
1194
+ async destroy(req, id) { await (await Post.findOrFail(id)).delete() }
1195
+ }
1196
+
1197
+ module.exports = PostController
1198
+ ```
1199
+
1200
+ Generate one with `kreftjs make:controller PostController --api` (also `--model Post`, `--route=api`, `--auth`).
1201
+
1202
+ ### The request
1203
+
1204
+ | Call | Gives |
1205
+ |---|---|
1206
+ | `req.input('name', default)`, `req.all()`, `req.only('a', 'b')`, `req.except(…)`, `req.has(…)`, `req.filled(…)` | Values from the body and the query. |
1207
+ | `req.boolean('x')`, `req.integer('page', 1)`, `req.string('q')` | A value in the right type. |
1208
+ | `await req.validate(rules)` | The same rules as `form()`, plus `unique:table,column` and `exists:table,column`. Returns only the checked fields, or answers `422`. |
1209
+ | `req.params`, `req.query`, `req.body`, `req.header('x')`, `req.bearerToken()`, `req.ip()`, `req.user` | The rest. |
1210
+ | `req.file('avatar').store('avatars')` | Saves an upload with a random name. |
1211
+ | `response(data, 201)`, `response(data).header('X', 'y')`, `abort(403, 'message')` | Answers. |
1212
+
1213
+ ### Form requests, resources and policies
1214
+
1215
+ For bigger endpoints, move rules and authorization out of the controller:
1216
+
1217
+ ```js
1218
+ // app/Http/Requests/StorePostRequest.js
1219
+ const { FormRequest } = require('kreftjs/server')
1220
+
1221
+ class StorePostRequest extends FormRequest {
1222
+ authorize(req) { return req.user != null } // false answers 403
1223
+ rules() { return { title: 'required|max:200', body: 'required' } }
1224
+ messages() { return { 'title.required': 'Give the post a title.' } }
1225
+ }
1226
+
1227
+ module.exports = StorePostRequest
1228
+ ```
1229
+
1230
+ ```js
1231
+ const data = await StorePostRequest.validate(req) // in a controller
1232
+ return PostResource.collection(await Post.paginate(15)) // { data, meta }
1233
+ await req.authorize('update', post) // app/Policies/PostPolicy.js
1234
+ ```
1235
+
1236
+ Generate them with `kreftjs make:request`, `kreftjs make:resource` and `kreftjs make:policy`.
1237
+
1238
+ ### Passing data from controllers to pages
1239
+
1240
+ A page receives data from the API with `load` (see [Fetching data](#fetching-data)). Return plain objects, collections or paginator results from controllers; hidden model fields (`static hidden`) are never included.
1241
+
1242
+ ## Models and the database
1243
+
1244
+ Models are Active Record classes, like Eloquent:
1245
+
1246
+ ```js
1247
+ // app/Models/Post.js
1248
+ const { Model, Str } = require('kreftjs/server')
1249
+
1250
+ class Post extends Model {
1251
+ static fillable = ['title', 'body', 'user_id'] // what create() and update() may fill
1252
+ static hidden = [] // never sent as JSON
1253
+ static casts = { published: 'boolean', meta: 'json' }
1254
+ static softDeletes = true // delete() fills deleted_at
1255
+
1256
+ user() { return this.belongsTo(require('./User')) }
1257
+ comments() { return this.hasMany(require('./Comment')) }
1258
+ tags() { return this.belongsToMany(require('./Tag')) }
1259
+
1260
+ static scopePublished(q) { q.where('published', true) }
1261
+ static async creating(post) { post.slug ??= Str.slug(post.title) }
1262
+ }
1263
+
1264
+ module.exports = Post
1265
+ ```
1266
+
1267
+ The table is the plural of the class name (`Post` is `posts`); set `static table = '…'` for another one.
1268
+
1269
+ ### Querying
1270
+
1271
+ ```js
1272
+ await Post.all()
1273
+ await Post.find(5) // or findOrFail(5): 404 when missing
1274
+ await Post.where('published', true).orderByDesc('id').limit(10).get()
1275
+ await Post.where('views', '>', 100).orWhere('pinned', true).count()
1276
+ await Post.query().published().with('user', 'comments.author').withCount('comments').paginate(15, page)
1277
+ await Post.query().whereHas('comments', (q) => q.where('approved', true)).get()
1278
+
1279
+ const post = await Post.create({ title: 'Hi' }) // fills created_at and updated_at
1280
+ await post.update({ title: 'Hello' })
1281
+ await post.delete()
1282
+
1283
+ await post.comments().create({ body: 'Nice' })
1284
+ await post.tags().sync([1, 2, 3]) // also attach(), detach(), toggle()
1285
+ ```
1286
+
1287
+ Relations: `hasOne`, `hasMany`, `belongsTo`, `belongsToMany` (with `pivot`), `hasManyThrough`. Soft deleted rows come back with `withTrashed()` and return with `restore()`.
1288
+
1289
+ Without a model:
1290
+
1291
+ ```js
1292
+ await DB.table('orders').join('users', 'users.id', '=', 'orders.user_id').groupBy('status').get()
1293
+ await DB.select('SELECT * FROM posts WHERE id = ?', [5])
1294
+ await DB.transaction(async () => { await Order.create({ … }); await Stock.decrement(…) }) // all or nothing
1295
+ ```
1296
+
1297
+ > [!WARNING]
1298
+ > Values always travel as parameters (`?`). Never build SQL by joining user input into a string. `whereRaw`, `selectRaw` and `orderByRaw` take raw SQL on purpose: pass user values through their parameter array.
1299
+
1300
+ ### Databases
1301
+
1302
+ The database comes from `DATABASE_URL` in `.env`:
1303
+
1304
+ ```env
1305
+ DATABASE_URL=sqlite:backend/database/app.db # built into Node.js, nothing to install
1306
+ DATABASE_URL=mysql://user:pass@localhost:3306/shop # npm i mysql2
1307
+ DATABASE_URL=postgres://user:pass@localhost:5432/shop # npm i pg
1308
+ ```
1309
+
1310
+ The same migrations and models work on all three.
1311
+
1312
+ ## Migrations, seeders and factories
1313
+
1314
+ A migration describes a table in JavaScript; the same file works on SQLite, MySQL and PostgreSQL:
1315
+
1316
+ ```js
1317
+ // database/migrations/2026_10_29_120000_create_posts_table.js
1318
+ module.exports = {
1319
+ up(schema) {
1320
+ schema.create('posts', (table) => {
1321
+ table.id()
1322
+ table.foreignId('author_id').constrained().cascadeOnDelete()
1323
+ table.string('title', 200).unique()
1324
+ table.text('body').nullable()
1325
+ table.boolean('published').default(false)
1326
+ table.json('meta').nullable()
1327
+ table.timestamps()
1328
+ })
1329
+ },
1330
+ down(schema) {
1331
+ schema.dropIfExists('posts')
1332
+ },
1333
+ }
1334
+ ```
1335
+
1336
+ Column types include `id`, `string`, `text`, `integer`, `bigInteger`, `decimal`, `float`, `boolean`, `date`, `dateTime`, `timestamp`, `timestamps`, `json`, `foreignId`. Plain `.sql` files in `database/migrations` also work (they run once each, in name order).
1337
+
1338
+ A seeder fills starting data:
1339
+
1340
+ ```js
1341
+ // database/seeders/DatabaseSeeder.js
1342
+ const { Seeder } = require('kreftjs/server')
1343
+ const Post = require('../../app/Models/Post')
1344
+
1345
+ class DatabaseSeeder extends Seeder {
1346
+ async run() {
1347
+ await Post.create({ title: 'Hello', author_id: 1 })
1348
+ await this.call(require('./CommentSeeder'))
1349
+ }
1350
+ }
1351
+
1352
+ module.exports = DatabaseSeeder
1353
+ ```
1354
+
1355
+ Factories make fake data for seeders and tests:
1356
+
1357
+ ```js
1358
+ await User.factory().count(10).has(Post.factory().count(3), 'posts').create()
1359
+ ```
1360
+
1361
+ ### Commands
1362
+
1363
+ | Command | What it does |
1364
+ |---|---|
1365
+ | `kreftjs migrate` | Runs the new migrations (`kreftjs dev` does this by itself). |
1366
+ | `kreftjs migrate:rollback` | Undoes the last batch (`--step 2` for the last two). |
1367
+ | `kreftjs migrate:fresh --seed` | Drops every table, migrates again, then seeds. |
1368
+ | `kreftjs migrate:status` | Which migrations ran. |
1369
+ | `kreftjs db:seed` | Runs `DatabaseSeeder` (`--class PostSeeder` for another). |
1370
+ | `kreftjs make:model Post -mfsc` | A model plus migration (`-m`), factory (`-f`), seeder (`-s`), controller (`-c`), page (`-p`), route (`-r`). |
1371
+ | `kreftjs make:migration create_posts_table` | `add_x_to_posts_table` makes a `schema.table` one. |
1372
+ | `kreftjs make:seeder`, `kreftjs make:middleware` | The files, in the right folders. |
1373
+
1374
+ > [!NOTE]
1375
+ > `migrate:fresh` and `db:wipe` destroy data. They are meant for development databases only.
1376
+
1377
+ ## Authentication
1378
+
1379
+ New projects come with every sign-in screen and the backend behind it:
1380
+
1381
+ | Page | Address | What it does |
1382
+ |---|---|---|
1383
+ | `pages/auth/Login.kreftjs` | `/login` | Log in with email and password. |
1384
+ | `pages/auth/Register.kreftjs` | `/register` | Make an account and get an email to verify the address. |
1385
+ | `pages/auth/ForgotPassword.kreftjs` | `/forgot-password` | Email a link to choose a new password. |
1386
+ | `pages/auth/ResetPassword.kreftjs` | `/reset-password/:token` | The link from that email. |
1387
+ | `pages/auth/VerifyEmail.kreftjs` | `/verify-email` | "Check your email", and the link from it opens `/verify-email/:id/:hash`. |
1388
+ | `pages/auth/ConfirmPassword.kreftjs` | `/confirm-password` | Type the password again before something serious. |
1389
+ | `pages/Dashboard.kreftjs`, `pages/Profile.kreftjs` | `/dashboard`, `/profile` | The first page after logging in; account settings. |
1390
+
1391
+ ### How a login works
1392
+
1393
+ - A login puts a token in an **HttpOnly cookie**. Page scripts cannot read it, so a script injected into a page cannot steal it. Only a hash of the token is stored in the database.
1394
+ - Every change (POST, PUT, PATCH, DELETE) also needs the `X-XSRF-TOKEN` header to match the `XSRF-TOKEN` cookie. `Kreftjs.api` does this for you.
1395
+ - A script or an app that sends `device_name` when it logs in gets an **API token** (`Authorization: Bearer 5|…`) instead.
1396
+ - Passwords are hashed with scrypt. Reset links expire after 60 minutes and sign the user out everywhere.
1397
+ - Session ids change on every login, so a session cannot be planted in a browser beforehand.
1398
+
1399
+ On the server:
1400
+
1401
+ ```js
1402
+ Route.middleware(['auth', 'verified']).group(() => {
1403
+ Route.get('/orders', [OrderController, 'index']) // req.user is the user
1404
+ })
1405
+
1406
+ Route.delete('/account', …).middleware(['auth', 'password.confirm']) // asks for the password again (423)
1407
+ ```
1408
+
1409
+ Helpers you can use anywhere in the backend:
1410
+
1411
+ ```js
1412
+ const { Hash, Str, Url, Mail, Crypt } = require('kreftjs/server')
1413
+
1414
+ Hash.make('secret') // store this, never the password
1415
+ Hash.check('secret', user.password) // true or false
1416
+ Str.random(40) // a random token
1417
+ Url.signed('/verify-email/5/abc', 60) // a link that works for 60 minutes, unchanged
1418
+ await Mail.send({ to, subject, text }) // printed in the terminal until MAIL_URL is set
1419
+ ```
1420
+
1421
+ ### Protections in the sign-in routes
1422
+
1423
+ - Visitors get `throttle:6,1`: six tries a minute per address.
1424
+ - Each **account** also locks after `LOGIN_MAX_FAILURES` (10) wrong passwords in `LOGIN_WINDOW_SECONDS` (15 minutes), whatever address the tries come from. After that, even the right password gets `429` until the window ends.
1425
+ - Two-factor codes are counted per account too: `TWO_FACTOR_MAX_FAILURES` (8) wrong codes lock it, and starting a new login does not reset the count.
1426
+ - "Forgot your password?" answers the same whether or not the address has an account.
1427
+ - Changing the email address needs a password confirmation from the last 3 hours (answer `423`), because the address is what password reset trusts.
1428
+
1429
+ ### Two-factor, devices and API tokens
1430
+
1431
+ - **Two-factor**: authenticator-app codes (TOTP) plus recovery codes. Each recovery code works once.
1432
+ - **Devices**: `/profile` lists the signed-in browsers, and any of them can be logged out.
1433
+ - **API tokens**: create them in `/profile` with the permissions `read`, `create`, `update`, `delete`. A token can only use the HTTP methods its permissions allow, on **every** route (`GET` needs `read`, `POST` needs `create`, `PUT`/`PATCH` need `update`, `DELETE` needs `delete`). Only browser sessions and `*` tokens can manage tokens, the password and the account.
1434
+
1435
+ ### Admin users
1436
+
1437
+ An admin is a user whose `is_admin` column is true. The test user is one; give the rights to others on the Users page, or with `kreftjs make:admin you@example.com`. Use `Route.middleware(['auth', 'admin'])` for admin-only routes.
1438
+
1439
+ ## Middleware
1440
+
1441
+ Middleware runs before the controller and can stop the request or change the answer:
1442
+
1443
+ ```js
1444
+ // app/Http/Middleware/EnsureIsAdmin.js ('ensure-is-admin' in a route, or 'ensure-is-admin:editor')
1445
+ const { abort } = require('kreftjs/server')
1446
+
1447
+ class EnsureIsAdmin {
1448
+ async handle(req, next, role = 'admin') {
1449
+ if (!req.user || req.user.role !== role) abort(403)
1450
+ const response = await next(req) // the controller runs here
1451
+ return response.header('X-Checked', 'yes')
1452
+ }
1453
+ }
1454
+
1455
+ module.exports = EnsureIsAdmin
1456
+ ```
1457
+
1458
+ A name in a route finds its file in `app/Http/Middleware`: `'auth'` is `Authenticate.js`, `'ensure-is-admin'` is `EnsureIsAdmin.js`.
1459
+
1460
+ ### Built in
1461
+
1462
+ | Middleware | What it does |
1463
+ |---|---|
1464
+ | `throttle:60,1` | Rate limiting: 60 requests a minute per visitor and route, then `429`. |
1465
+ | `csrf` | Checks the CSRF token on POST, PUT, PATCH and DELETE (`419` when wrong). |
1466
+ | `can:ability,model` | Policy checks through `Gate.authorize()`. |
1467
+ | `auth`, `guest`, `verified`, `password.confirm` | The sign-in set from `app/Http/Middleware`. |
1468
+
1469
+ For every route, list middleware in `app/Http/Kernel.js`:
1470
+
1471
+ ```js
1472
+ module.exports = {
1473
+ middleware: ['throttle:120,1'],
1474
+ }
1475
+ ```
1476
+
1477
+ Exempt a route with `.withoutMiddleware('csrf')`, or `new VerifyCsrfToken({ except: ['webhooks/*'] })`.
1478
+
1479
+ ## Queues, mail, cache and files
1480
+
1481
+ Each tool has its own folder in `backend/app/` and a `kreftjs make:` command:
1482
+
1483
+ ```js
1484
+ await dispatch(new SendWelcome(user)) // app/Jobs: runs in the background (tries, backoff, failed())
1485
+ await event(new OrderShipped(order)) // app/Listeners
1486
+ await Mail.to(user).send(new WelcomeMail(user)) // app/Mail: subject, text, html or markdown({ … })
1487
+ await user.notify(new InvoicePaid(invoice)) // app/Notifications: 'mail' and 'database' channels
1488
+ await Cache.remember('stats', 600, () => loadStats()) // database, file, memory or redis (CACHE_STORE)
1489
+ const path = await req.file('avatar').store('avatars') // uploads; Storage.url(path) is /storage/avatars/…
1490
+ ```
1491
+
1492
+ ### Jobs
1493
+
1494
+ `Job.dispatch(...)` returns an object you can chain (`delay(seconds)`, `onQueue(name)`) and then `await`:
1495
+
1496
+ ```js
1497
+ await SendWelcome.dispatch(user)
1498
+ await SendWelcome.dispatch(user).delay(60).onQueue('mail')
1499
+ ```
1500
+
1501
+ > [!WARNING]
1502
+ > `dispatch` sends the job when you `await` it (or call `.then`). Calling it and forgetting the `await` sends nothing.
1503
+
1504
+ Queued jobs, listeners, mail and notifications run in the background. `kreftjs dev` and `kreftjs serve` work through the `jobs` table every second. Control workers with `kreftjs queue:work`, `queue:failed`, `queue:retry all`, `queue:flush`. Choose the driver with `QUEUE_CONNECTION` (`database`, `redis`, `sync`).
1505
+
1506
+ ### Uploads
1507
+
1508
+ `req.file('avatar').store('avatars')` saves with a random name and never trusts the original one. Only known plain-data extensions (images, PDF, text, office files, audio, video, archives) keep their extension; anything else is stored as `.bin`. Use `file.isImage()` to also check the file's real header, not only its name.
1509
+
1510
+ Files on the public disk are served from `/storage/…` as downloads of plain data, so an uploaded `.html` or `.svg` can never run scripts on your site.
1511
+
1512
+ ### Scheduling
1513
+
1514
+ ```js
1515
+ // backend/routes/console.js
1516
+ Schedule.call(async () => { await Post.onlyTrashed().forceDelete() }).daily().name('clean-trash')
1517
+ Schedule.job(new SendDailyReport()).weekdays().at('08:00')
1518
+ Schedule.command('send:report Daily').hourly()
1519
+ ```
1520
+
1521
+ `kreftjs dev` and `kreftjs serve` check the schedule every minute. Set `SCHEDULER=off` to run it from cron with `kreftjs schedule:run`; `kreftjs schedule:list` shows what is planned.
1522
+
1523
+ ### Redis
1524
+
1525
+ Kreft includes its own Redis client (RESP2 over TCP or TLS) with no extra package. It works with Redis, Valkey and hosted Redis services.
1526
+
1527
+ ```env
1528
+ CACHE_STORE=redis
1529
+ QUEUE_CONNECTION=redis
1530
+ REDIS_URL=rediss://default:secret@host:6379/0 # or REDIS_HOST, REDIS_PORT, REDIS_USERNAME, REDIS_PASSWORD, REDIS_DB
1531
+ REDIS_PREFIX=kreft_
1532
+ ```
1533
+
1534
+ ```bash
1535
+ kreftjs redis:ping # PONG (1ms) — 127.0.0.1:6379/0
1536
+ ```
1537
+
1538
+ Redis also gives you atomic locks (`Cache.lock('report', 30)`) and publish/subscribe.
1539
+
1540
+ ## Configuration
1541
+
1542
+ Settings come from `.env`, `backend/config/*.js` and defaults, through one helper:
1543
+
1544
+ ```js
1545
+ const { config } = require('kreftjs/server')
1546
+
1547
+ config('app.name', 'My app')
1548
+ config('database.default', 'sqlite')
1549
+ config.has('redis.connections.default')
1550
+ config.set('app.debug', false)
1551
+ ```
1552
+
1553
+ Files in `backend/config/`: `app.js`, `auth.js`, `database.js`, `redis.js`, `session.js`. See [Environment variables](#environment-variables) for what `.env` controls.
1554
+
1555
+ ### Sessions and inactivity
1556
+
1557
+ Two layers log out a user who walks away:
1558
+
1559
+ ```env
1560
+ SESSION_LIFETIME=120 # minutes of inactivity before the server drops the session
1561
+ PUBLIC_SESSION_TIMEOUT=15 # minutes before the page itself sends the user to /login
1562
+ PUBLIC_SESSION_WARNING=2 # minutes of warning before that
1563
+ AUTH_TOKEN_DAYS=30 # a token unused for this many days stops working
1564
+ ```
1565
+
1566
+ `PUBLIC_` values are copied into the browser (`Kreftjs.env`); never put secrets in them.
1567
+
1568
+ ### Behind a proxy
1569
+
1570
+ The visitor's address comes from the connection. Behind nginx, Cloudflare or a load balancer, list your proxies:
1571
+
1572
+ ```env
1573
+ TRUSTED_PROXIES=10.0.0.1 # or *
1574
+ TRUSTED_PROXY_HOPS=1 # with *: how many proxies stand in front
1575
+ ```
1576
+
1577
+ `X-Forwarded-For` is read from the right, so the visitor cannot choose their own address.
1578
+
1579
+ ## Testing
1580
+
1581
+ Backend code is tested with Node's own test runner and an in-memory SQLite database, so tests are fast and never touch your real data.
1582
+
1583
+ ```json
1584
+ {
1585
+ "scripts": { "test": "node --test \"tests/**/*.test.js\"" }
1586
+ }
1587
+ ```
1588
+
1589
+ ```js
1590
+ // tests/models/post.test.js
1591
+ const test = require('node:test')
1592
+ const assert = require('node:assert/strict')
1593
+ const { createSchema, setConnection } = require('kreftjs/server')
1594
+ const { connect } = require('kreft/bin/db.js')
1595
+ const Post = require('../../backend/app/Models/Post')
1596
+
1597
+ test.before(async () => {
1598
+ const db = await connect('sqlite::memory:') // a new empty database for this test file
1599
+ setConnection(db)
1600
+ await createSchema(db).create('posts', (table) => {
1601
+ table.id()
1602
+ table.string('title')
1603
+ table.timestamps()
1604
+ })
1605
+ })
1606
+
1607
+ test('a post can be created and found', async () => {
1608
+ const post = await Post.create({ title: 'Hello' })
1609
+ assert.equal((await Post.find(post.id)).title, 'Hello')
1610
+ })
1611
+ ```
1612
+
1613
+ To test a whole project's tables, run its real migrations instead of building the schema by hand: `migrator.migrate(db, 'backend/database/migrations')`.
1614
+
1615
+ Component pages for the browser live in `frontend/tests/` and are opened with `kreftjs dev`.
1616
+
1617
+ # Tooling
1618
+
1619
+ ## Command line
1620
+
1621
+ Run `kreftjs list` to see every command your installed version has, and `kreftjs help` for the options of the main ones.
1622
+
1623
+ ### The main commands
1624
+
1625
+ | Command | What it does |
1626
+ |---|---|
1627
+ | `kreftjs new my-app` | Makes a project (`--js`, `--ts`, `--db sqlite`, `--ui tailwind`, `--no-auth`). |
1628
+ | `kreftjs dev` | Development server with hot reload (`--port`, `--host`, `--db`, `--api`, `--server`, `--backend`, `--seed`, `--no-hot`, `--gc`). |
1629
+ | `kreftjs build` | Compiles the app into plain files in `dist/` (`--out`, `--base`, `--prerender`). |
1630
+ | `kreftjs serve [dist]` | Runs a built app for real users. Answers everyone, serves only the tables you list. |
1631
+ | `kreftjs update` | Updates npm packages and refreshes `kreftjs.js` (`--core`, `--npm`, `--latest`, `--check`). |
1632
+ | `kreftjs add <package>` | A safer `npm install`. See [Packages](#packages). |
1633
+ | `kreftjs about`, `kreftjs env`, `kreftjs log`, `kreftjs tinker` | Versions and folders, environment, logs, a console with your models (`await User.count()`). |
1634
+ | `kreftjs down --secret=x` / `kreftjs up` | Maintenance mode: visitors get `503`, and `/x` lets you in. |
1635
+ | `kreftjs vscode` | Installs colors, snippets and folding for `.kreftjs` files in VS Code, Cursor and VSCodium. |
1636
+
1637
+ ### Generators and database
1638
+
1639
+ | Group | Commands |
1640
+ |---|---|
1641
+ | `make:` | `model`, `controller`, `page`, `routes`, `migration`, `seeder`, `factory`, `middleware`, `request`, `resource`, `policy`, `rule`, `job`, `event`, `listener`, `mail`, `notification`, `observer`, `command`, `lang`, `errors`, `admin`, `electron`, `vite` |
1642
+ | `migrate` | `migrate`, `:rollback`, `:fresh`, `:refresh`, `:reset`, `:status`, `:file`, `:undo` |
1643
+ | `db:` | `db:seed`, `db:wipe` |
1644
+ | `queue:` | `work`, `start`, `stop`, `pause`, `resume`, `restart`, `status`, `failed`, `retry`, `flush`, `clear`, `table` |
1645
+ | `schedule:` | `run`, `work`, `list` |
1646
+ | Others | `route:list`, `cache:clear`, `storage:link`, `key:generate`, `event:list`, `log:clear` |
1647
+
1648
+ `kreftjs dev --seed` seeds a brand-new database the first time. `kreftjs key:generate` writes a 256-bit `APP_KEY` into `.env` (`--show` prints it instead).
1649
+
1650
+ ### Development server safety
1651
+
1652
+ - `kreftjs dev` answers only this computer (`127.0.0.1`) and only requests whose `Host` is a local name (`localhost`, `127.0.0.1`, `::1`, `*.localhost`). This stops DNS-rebinding attacks, where a web page tries to reach your dev server through its own domain name.
1653
+ - To reach it through a tunnel or another name, set `KREFT_ALLOWED_HOSTS=my-tunnel.example`. To share it on your network, use `--host 0.0.0.0`.
1654
+ - Requests that change data in `--api` or `--db` mode are refused when they come from another web site (a browser `Origin` that is not local).
1655
+ - **Moving or deleting the project folder.** The operating system cannot refuse a delete, so the server says so after it happens: if the project folder is deleted or moved to the Trash while `kreftjs dev` or `kreftjs serve` runs, it stops at once with a message. Any `kreftjs` command started from a deleted folder or one in the Trash refuses to run. Stop the server with <kbd>Ctrl</kbd>+<kbd>C</kbd> before you move or delete a project.
1656
+
1657
+ ## Developer tools
1658
+
1659
+ Everything here comes with Kreft. Nothing else to install.
1660
+
1661
+ **Hot reload.** While `kreftjs dev` runs, save a `.kreftjs` file and only that component is swapped; its state stays. A `.css` file is reloaded without a page refresh, and an error shows an overlay with the file and line.
1662
+
1663
+ **Kreft DevTools.** The **S** button in the corner (or <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>D</kbd>) opens a panel:
1664
+
1665
+ | Tab | Shows |
1666
+ |---|---|
1667
+ | Components | The live tree. Pick one to see props, state and derived values, and change state right there. |
1668
+ | Stores | Every store's values, which you can change, and `$reset()`. |
1669
+ | Route | The current route and every route, with a button to go there. |
1670
+ | Timeline | Store actions and bus messages as they happen. |
1671
+
1672
+ The button and the panel only exist under `kreftjs dev`; built apps never include them.
1673
+
1674
+ **Types.** `kreftjs.d.ts` describes everything in `Kreft`, so TypeScript and your editor know it. `kreftjs new` copies it into new projects, and `kreftjs update --core` refreshes it.
1675
+
1676
+ ### Error monitor
1677
+
1678
+ In development, an uncaught error on the server shows an interactive page instead of a bare message:
1679
+
1680
+ - A stack explorer that hides vendor frames and highlights yours.
1681
+ - A source preview around the failing line.
1682
+ - A suggestion engine for common mistakes (a method typo on a model, reading a property of `undefined`, a missing table that needs `kreftjs migrate`).
1683
+ - The request: method, URL, route and query values, body, headers and client address. Passwords, tokens, cookies and CSRF values are masked.
1684
+ - Copy the error as Markdown for an issue, or as JSON.
1685
+
1686
+ The page is only shown when `APP_ENV` is not `production` and `APP_DEBUG` is true. **In production the same error answers a plain `Server error.`** with no stack or file names.
1687
+
1688
+ Frontend errors use the same design through `errors/ErrorMonitor.kreftjs`; pass it a caught exception:
1689
+
1690
+ ```html
1691
+ import ErrorMonitor from 'errors/ErrorMonitor.kreftjs'
1692
+ state caught = null
1693
+
1694
+ <view>
1695
+ <error-monitor k-if="caught" error={caught} status={500} title={caught.name} message={caught.message}></error-monitor>
1696
+ </view>
1697
+ ```
1698
+
1699
+ ## Build and deploy
1700
+
1701
+ `kreftjs build` compiles your app ahead of time:
1702
+
1703
+ ```bash
1704
+ npm run build # or: kreftjs build
1705
+ npm run preview # check the result: kreftjs dev dist
1706
+ ```
1707
+
1708
+ | File | What it is |
1709
+ |---|---|
1710
+ | `index.html` | Your page, now loading the files below. |
1711
+ | `kreftjs.js` | The framework. |
1712
+ | `app.js` | Every `.kreftjs` file, already compiled. |
1713
+ | `app.css` | All `<style>` blocks; scoped ones already scoped. |
1714
+ | `main.js` | The inline `<script>` code from `index.html`. |
1715
+ | `deps/`, `vendor/` | The npm packages and CSS your `.kreftjs` files import, minified. |
1716
+
1717
+ What you get:
1718
+
1719
+ - **A faster first load.** The browser downloads one `app.js` and compiles nothing.
1720
+ - **A strict Content-Security-Policy works.** Every expression becomes a real function, so the built app never uses `eval` or `new Function`.
1721
+ - **Errors before you deploy.** A mistake in any `.kreftjs` file stops the build and names the file and line.
1722
+
1723
+ Options: `kreftjs build ./site --out public` builds another folder, and `--base /` changes `<base href>`. Upload the output folder to any static host.
1724
+
1725
+ ### Ready HTML for every page: `--prerender`
1726
+
1727
+ ```bash
1728
+ npm i -D jsdom # once per project
1729
+ kreftjs build --prerender # every route without :params
1730
+ kreftjs build --prerender /users/1,/users/2 # and these addresses too
1731
+ ```
1732
+
1733
+ Each page is opened in a simulated browser, its data loaded, and the result saved as `dist/<page>/index.html`, so visitors and search engines get finished HTML.
1734
+
1735
+ The tags a page sets with `.seo({...})` or `meta` (description, canonical, Open Graph, Twitter, `hreflang`, JSON-LD, `lang` and `dir`) are saved in the page's HTML too, so link previews and crawlers that don't run JavaScript see them. A canonical written in `index.html` is kept for the home page only. A page where the app shows its error page is skipped.
1736
+
1737
+ Pages that come from your data (`/posts/:slug`) need their addresses: `kreftjs build --prerender --urls urls.js`. The file is `urls.json` (a list), `urls.txt` (one address per line), or `urls.js` (a list, or an async function that returns it).
1738
+
1739
+ ### SEO for pages from your database: `backend/routes/seo.js`
1740
+
1741
+ A page whose content comes from a table can't be known at build time. List what the server should put in `<head>` for an address:
1742
+
1743
+ ```js
1744
+ // backend/routes/seo.js
1745
+ const Post = require('../app/Models/Post')
1746
+
1747
+ module.exports = {
1748
+ '/': { title: 'My shop', description: 'Everything for the home' },
1749
+ '/posts/:slug': async ({ slug }) => {
1750
+ const post = await Post.where('slug', slug).first()
1751
+ if (!post) return null // a real 404, with noindex
1752
+ return {
1753
+ title: post.title,
1754
+ description: post.excerpt,
1755
+ og: { image: post.image },
1756
+ schema: { '@type': 'Article', headline: post.title },
1757
+ alternates: { en: `https://example.com/posts/${slug}`, ar: `https://example.com/ar/posts/${slug}` },
1758
+ }
1759
+ },
1760
+ // the addresses of your data pages, for `kreftjs sitemap`: '/path' or { path, lastmod }
1761
+ urls: async () => (await Post.all()).map((p) => ({ path: '/posts/' + p.slug, lastmod: p.updated_at })),
1762
+ }
1763
+ ```
1764
+
1765
+ `kreftjs dev` and `kreftjs serve` answer a page request with `index.html` plus the tags of the first matching address (`:param` and a trailing `/*` work). Set `APP_URL` and each page also gets its `canonical`. An address that isn't listed is the normal page. The browser keeps these tags until the first navigation, then the router's own tags take over.
1766
+
1767
+ ### Whole pages for crawlers
1768
+
1769
+ A crawler or link preview (Googlebot, Bingbot, WhatsApp, Facebook, X, Slack, Telegram …) that doesn't run JavaScript would see an empty `<div id="app">`. For these visitors `kreftjs dev` and `kreftjs serve` open the page in the same simulated browser as `--prerender`, wait until its data has loaded, and answer with the page already drawn. If the app shows its `errors/404.kreftjs` page, the answer has status 404. A page that `kreftjs build --prerender` already drew is sent as it is. Everyone else gets the normal page.
1770
+
1771
+ ```bash
1772
+ npm i -D jsdom # once per project
1773
+ KREFT_SSR=bots # default: crawlers and link previews only
1774
+ KREFT_SSR=all # every visitor
1775
+ KREFT_SSR=off # never
1776
+ KREFT_SSR_TTL=300 # seconds a drawn page is kept (kreftjs dev never keeps them)
1777
+ ```
1778
+
1779
+ At most two pages are drawn at a time; the other crawlers wait for their turn (up to 15 seconds, 20 at once), and the same address asked twice is drawn once. A page that takes longer than 15 seconds, fails, or is not a page of the app is not tried again for 30 seconds, and the normal page is sent meanwhile, as it is when jsdom is missing. The query string is part of the address (`/search?q=sofa` is its own page). Images, styles and fonts from other sites are not downloaded while drawing, scripts are. Responses carry `Vary: User-Agent` and the same security headers as every other page. The simulated browser introduces itself with a secret `User-Agent` that changes on every start, so it is never drawn again.
1780
+
1781
+ ### Sitemap and robots.txt: `kreftjs sitemap`
1782
+
1783
+ ```bash
1784
+ kreftjs sitemap --url https://example.com # public/sitemap.xml and public/robots.txt
1785
+ kreftjs sitemap --url https://example.com --urls urls.js # and the addresses in that file
1786
+ ```
1787
+
1788
+ It lists the routes of `Kreftjs.router({ routes })` in `index.html` and `routes/web.kreftjs`, skipping redirects and patterns like `/posts/:id`, plus the `urls` of `backend/routes/seo.js` and `--urls`. A `lastmod` is written only when you give one (a database date such as `2026-01-02 10:00:00` becomes `2026-01-02`). A route can list `alternates: { en: '/about', ar: '/ar/about' }` for `xhtml:link` entries.
1789
+
1790
+ ### Deploying the backend
1791
+
1792
+ ```bash
1793
+ NODE_ENV=production kreftjs serve dist
1794
+ ```
1795
+
1796
+ Before you do, work through the [Production checklist](#production-checklist).
1797
+
1798
+ ## TypeScript
1799
+
1800
+ TypeScript works in the browser (`.kreftjs` files and `.ts` modules) and on the server (`routes/api.ts`, `app/**/*.ts`, migrations, seeders).
1801
+
1802
+ ```bash
1803
+ kreftjs new my-app --ts
1804
+ ```
1805
+
1806
+ Inside a `.kreftjs` file you can write `interface`, `type`, `enum`, `import type`, and typed `prop`, `state`, `derive` and `saved`. `kreftjs make:*` writes `.ts` files when the project has a `tsconfig.json` (or with `--ts`; `--js` forces JavaScript).
1807
+
1808
+ Execution needs no setup: Kreft uses `esbuild` if installed, otherwise Node's built-in type stripping (Node 22.6+), otherwise its own zero-dependency stripper.
1809
+
1810
+ > [!NOTE]
1811
+ > Kreft removes types; it does not check them. Run `tsc --noEmit` in your CI for type checking.
1812
+
1813
+ ## Vite and Electron
1814
+
1815
+ **Vite.** If your team prefers the Vite ecosystem, Kreft has a plugin that compiles `.kreftjs` files on the fly:
1816
+
1817
+ ```bash
1818
+ kreftjs vite:init # writes vite.config.js with the Kreft plugin
1819
+ kreftjs vite:dev # Vite dev server with the backend API proxied
1820
+ kreftjs vite:build # optimised bundle with code splitting
1821
+ kreftjs vite:preview
1822
+ ```
1823
+
1824
+ **Electron.** Turn the same app into a desktop application. See [Desktop apps with Electron](#desktop-apps-with-electron) below.
1825
+
1826
+ ## Desktop apps with Electron
1827
+
1828
+ Kreft can package your app as a desktop application for Windows, macOS and Linux. The pages are the same `.kreftjs` files you already have; Electron only supplies the window.
1829
+
1830
+ ### Quick start
1831
+
1832
+ ```bash
1833
+ kreftjs new my-app --electron # a new app with desktop support
1834
+ cd my-app && npm install
1835
+ kreftjs electron:dev # dev server plus a desktop window, with hot reload
1836
+ kreftjs electron:build # an installer in dist-electron/
1837
+ ```
1838
+
1839
+ Add desktop support to an existing app instead:
1840
+
1841
+ ```bash
1842
+ kreftjs electron:init # alias: kreftjs make:electron
1843
+ npm install # installs electron and electron-builder
1844
+ ```
1845
+
1846
+ ### Commands
1847
+
1848
+ | Command | What it does |
1849
+ |---|---|
1850
+ | `kreftjs electron:init` | Writes `electron/main.js` and `electron/preload.js` and adds the scripts, the `electron` and `electron-builder` dev dependencies and a `build` block to `package.json`. Files that already exist are never overwritten. It also removes the Google Fonts links from `index.html`, so the app starts without a network. |
1851
+ | `kreftjs electron:dev` | Starts `kreftjs dev` and opens a window on it. Closing the window stops the server. Flags after the command go to the dev server. |
1852
+ | `kreftjs electron:build` | Runs `kreftjs build --base ./`, then `electron-builder`. |
1853
+
1854
+ `kreftjs electron` is the same as `kreftjs electron:dev`. If the `electron/` folder is missing, both commands create it first.
1855
+
1856
+ ### Project layout
1857
+
1858
+ ```text
1859
+ my-app/
1860
+ ├── electron/
1861
+ │ ├── main.js the main process: creates the window, handles the desktop functions
1862
+ │ └── preload.js the safe bridge exposed to your pages as window.kreftDesktop
1863
+ ├── dist/ the static build the packaged app loads
1864
+ ├── dist-electron/ installers written by electron-builder
1865
+ └── package.json "main": "electron/main.js" and the "build" block
1866
+ ```
1867
+
1868
+ ### How it runs
1869
+
1870
+ - **Development.** The window loads `http://localhost:8000` (change it with `PORT`). Hot reload, the DevTools button and the error overlay work as in the browser.
1871
+ - **Packaged.** The window loads `dist/index.html` from disk. That is a static build, so the Kreft backend (`routes/`, the database, `kreftjs serve`) is **not** inside the app. Point the API client at a server you host:
1872
+
1873
+ ```js
1874
+ const api = Kreftjs.api.create({ base: 'https://api.example.com' })
1875
+ ```
1876
+
1877
+ Use a `PUBLIC_` value from `.env` for the address, so development and production can differ. Remember that CORS on your server must allow the app.
1878
+
1879
+ ### Detecting the desktop app
1880
+
1881
+ ```js
1882
+ if (Kreftjs.isElectron) {
1883
+ const info = await Kreftjs.desktop.getInfo() // { name, version, platform, arch, isPackaged }
1884
+ }
1885
+ ```
1886
+
1887
+ `Kreftjs.desktop` is `null` in a normal browser, so the same page works in both.
1888
+
1889
+ ### The desktop bridge
1890
+
1891
+ `window.kreftDesktop` (also `Kreftjs.desktop`) is the only way your pages reach the system. Nothing else from Node.js or Electron is exposed.
1892
+
1893
+ | Function | Result |
1894
+ |---|---|
1895
+ | `getInfo()` | `{ name, version, platform, arch, isPackaged }` |
1896
+ | `minimize()`, `maximize()`, `close()` | Control the window. `maximize()` toggles. |
1897
+ | `openDevTools(mode)`, `closeDevTools()`, `toggleDevTools()` | DevTools; `mode` is `right`, `bottom`, `left` or `detach`. |
1898
+ | `showMessageBox(options)` | A native message dialog. |
1899
+ | `showOpenDialog(options)` | A native "open file" dialog. |
1900
+ | `showSaveDialog(options)` | A native "save file" dialog. |
1901
+
1902
+ Every call returns a promise. The dialog functions pass on only a fixed list of options (for example `title`, `filters`, `properties`); anything else is dropped.
1903
+
1904
+ ```kreftjs
1905
+ state path = ''
1906
+
1907
+ async function pick() {
1908
+ const r = await Kreftjs.desktop.showOpenDialog({
1909
+ title: 'Choose a file',
1910
+ filters: [{ name: 'Images', extensions: ['png', 'jpg'] }],
1911
+ properties: ['openFile'],
1912
+ })
1913
+ if (!r.canceled) path = r.filePaths[0]
1914
+ }
1915
+ ```
1916
+
1917
+ ### Adding your own desktop function
1918
+
1919
+ 1. In `electron/main.js`, add a handler: `ipcMain.handle('app:ping', () => 'pong')`.
1920
+ 2. In `electron/preload.js`, add it to the `desktop` object: `ping: () => ipcRenderer.invoke('app:ping')`.
1921
+ 3. Call `Kreftjs.desktop.ping()` from a page.
1922
+
1923
+ Expose one narrow function per task. Never pass a path or a command from a page straight to the file system or a shell without checking it in `main.js`.
1924
+
1925
+ ### DevTools
1926
+
1927
+ They are hidden by default. Open them with a flag or an environment variable:
1928
+
1929
+ ```bash
1930
+ kreftjs electron:dev --devtools # right side
1931
+ kreftjs electron:dev --devtools=bottom # bottom, left or detach
1932
+ kreftjs electron:dev --no-devtools
1933
+ ELECTRON_DEVTOOLS=bottom kreftjs electron:dev
1934
+ ```
1935
+
1936
+ In development, F12 or Ctrl+Shift+I toggles them.
1937
+
1938
+ ### Packaging and installers
1939
+
1940
+ `electron:build` writes to `dist-electron/`. The defaults in the `build` block of `package.json` are:
1941
+
1942
+ | System | Targets |
1943
+ |---|---|
1944
+ | Windows | `nsis` installer, `portable` |
1945
+ | macOS | `dmg`, `zip` |
1946
+ | Linux | `AppImage`, `deb` |
1947
+
1948
+ Edit `appId`, `productName` and the targets there. Add an `icon` per system (see the electron-builder documentation). Build for each system on that system: a macOS installer needs a Mac. Code signing and notarisation (macOS) or a signing certificate (Windows) are set up through electron-builder and are needed before you give the app to other people.
1949
+
1950
+ ### Security
1951
+
1952
+ The window follows Electron's security guidance:
1953
+
1954
+ - `contextIsolation` and `sandbox` are on; `nodeIntegration` is off.
1955
+ - Links to other sites open in the system browser, and new windows are denied.
1956
+ - Navigation away from the app is blocked: only the dev server (development) or files inside `dist/` (packaged) can load.
1957
+ - The bridge exposes named functions only, with the option lists described above.
1958
+
1959
+ Do not turn these off to make something work. Add a bridge function instead.
1960
+
1961
+ ### Troubleshooting
1962
+
1963
+ | Problem | Fix |
1964
+ |---|---|
1965
+ | `Electron is not installed locally` | `kreftjs add -D electron` |
1966
+ | `electron-builder is not installed` | `kreftjs add -D electron-builder` |
1967
+ | A white window in the packaged app | Run `electron:build` again; the build must use `--base ./`. Check that `dist/index.html` exists. |
1968
+ | API calls fail in the packaged app | The backend is not bundled. Set an absolute `base` for the API client (see above). |
1969
+ | The window opens before the server is ready | It waits up to 15 seconds. Check the terminal for a dev server error. |
1970
+ | Port 8000 is taken | Run with `PORT=3000 kreftjs electron:dev`. |
1971
+
1972
+ ## Packages
1973
+
1974
+ `kreftjs add` is a safer `npm install`:
1975
+
1976
+ ```bash
1977
+ kreftjs add pg mysql2 # install packages
1978
+ kreftjs add -D vitest # a development dependency
1979
+ kreftjs add --dry-run some-package # inspect without installing
1980
+ kreftjs add --allow-scripts better-sqlite3 # allow build scripts for a trusted native package
1981
+ kreftjs add --force dotenv # ignore a warning
1982
+ ```
1983
+
1984
+ What it checks before and after installing:
1985
+
1986
+ 1. **Redundancy.** It warns when Kreft already does the job natively (`dotenv`, `cors`, `body-parser`, `node-fetch`…).
1987
+ 2. **Install scripts.** Packages install with `--ignore-scripts`, so a `postinstall` hook cannot run code, unless you pass `--allow-scripts` or the package is on the short list of known native-build packages.
1988
+ 3. **Typosquatting.** Names close to popular ones (`axois`, `lodsh`, `crossenv`) stop the install.
1989
+ 4. **Look-alike characters.** Names with non-ASCII or homoglyph characters are refused, as are shell metacharacters.
1990
+ 5. **Registry age.** A package published in the last 14 days is flagged.
1991
+ 6. **Exact versions.** `--save-exact` is the default (`--no-exact` to turn it off).
1992
+ 7. **Audit.** `npm audit` runs afterwards and reports findings.
1993
+
1994
+ ## MCP for AI agents
1995
+
1996
+ Kreft includes a Model Context Protocol server, so an AI coding agent can see your project's routes and schema, and query the database read-only.
1997
+
1998
+ | Tool or resource | What it gives |
1999
+ |---|---|
2000
+ | `query_database` | Runs one read-only `SELECT`. |
2001
+ | `list_routes` | Every route with methods, name and middleware. |
2002
+ | `server_health` | Uptime and memory. |
2003
+ | `kreft://routes`, `kreft://schema` | The route catalogue; the tables, columns and keys. |
2004
+
2005
+ ### Local use (stdio)
2006
+
2007
+ ```bash
2008
+ kreftjs mcp
2009
+ ```
2010
+
2011
+ ```json
2012
+ {
2013
+ "mcpServers": {
2014
+ "kreft": { "command": "kreft", "args": ["mcp"], "cwd": "/path/to/your-project" }
2015
+ }
2016
+ }
2017
+ ```
2018
+
2019
+ ### Over HTTP
2020
+
2021
+ `kreftjs dev` and `kreftjs serve` also answer at `/mcp`. Set a token and send it:
2022
+
2023
+ ```env
2024
+ MCP_TOKEN=a-long-random-secret
2025
+ ```
2026
+
2027
+ ```bash
2028
+ curl -H "Authorization: Bearer a-long-random-secret" -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' http://localhost:8000/mcp
2029
+ ```
2030
+
2031
+ ### What protects it
2032
+
2033
+ - **`MCP_TOKEN`** is required whenever it is set. The `APP_KEY` is never accepted in its place.
2034
+ - **In production** (`NODE_ENV` or `APP_ENV` = `production`), the endpoint is off (`403`) until `MCP_TOKEN` is set.
2035
+ - **In development without a token**, only a direct local client is accepted: a loopback address, a `localhost` Host header, and no proxy headers (`X-Forwarded-*`). A remote address, a proxy or a tunnel needs `MCP_TOKEN`.
2036
+ - **Browsers** from foreign origins are refused. `MCP_ALLOWED_ORIGIN` adds one origin.
2037
+ - **`query_database` is limited**: one `SELECT`, no comments or `;`, no mutations or file functions, no `SELECT … INTO`, and no sleeping or locking. It refuses columns and tables that hold credentials (anything named like a password, token, secret, hash or authentication, and the tokens, sessions and jobs tables), whole-row JSON building, and the database server's own accounts and sessions (`mysql.*`, `performance_schema`, `sys`, `pg_authid`…). Results are masked again by shape (password hashes). The query runs in a transaction that is always rolled back, with a time limit of 5 seconds (`MCP_QUERY_TIMEOUT_MS`).
2038
+
2039
+ > [!WARNING]
2040
+ > The MCP database tool can still read every other table. Use a database account with read-only access and no access to credentials, and keep the token out of version control.
2041
+
2042
+ ## Memory watchdog
2043
+
2044
+ Long-running servers can slowly collect memory. Start with `--gc` (or `AUTO_GC=1`) and Kreft samples the heap every 30 seconds (`AUTO_GC_INTERVAL_MS`). When it passes `AUTO_GC_THRESHOLD_MB`, Kreft clears its own caches and runs garbage collection. This is a safety net; find the leak if you see it often.
2045
+
2046
+ # Security
2047
+
2048
+ ## Security overview
2049
+
2050
+ Kreft is built so the safe thing is the default. This chapter lists what is protected, where, and what stays your job.
2051
+
2052
+ ### In the browser
2053
+
2054
+ - **Escaped views.** Everything in `{ }`, `k-text` and attributes is shown as text. `<script>` in a user's bio is only text.
2055
+ - **`k-html` is cleaned**, not trusted. Kreft parses the HTML the way the browser does, in a document that runs nothing, then removes scripts, frames, forms, SVG and MathML, every `on…` attribute, `srcdoc`, and addresses that start with `javascript:` or an unsafe `data:`. Even so, bind only HTML you trust or that the server already cleaned.
2056
+ - **A strict Content-Security-Policy works** with built apps. `kreftjs build` turns every expression into a function ahead of time, so there is no `eval` and no `new Function`. Without a build, views compile in the browser and a strict CSP blocks that.
2057
+ - **Prototype pollution is blocked.** Paths and keys such as `__proto__`, `constructor` and `prototype` are ignored in views, translations and JSON bodies.
2058
+ - **A login cookie is HttpOnly**, so a script on your page cannot read it.
2059
+
2060
+ ### On the server
2061
+
2062
+ #### CSRF
2063
+
2064
+ Kreft uses the double-submit cookie pattern. Any safe request gets an `XSRF-TOKEN` cookie; any request that changes data (POST, PUT, PATCH, DELETE) from a browser must send the same value back in `X-XSRF-TOKEN` (or `X-CSRF-TOKEN`, a `_token` field). The comparison is constant-time, and a wrong or missing token answers `419`.
2065
+
2066
+ - `Kreftjs.api` sends the header by itself. HTML forms add `<input type="hidden" name="_token" value="{csrfToken}">`.
2067
+ - `Route.csrfCookie('/csrf-cookie')` gives a single-page app an endpoint to get the cookie first.
2068
+ - Exempt only what must be (webhooks): `.withoutMiddleware('csrf')`.
2069
+ - Requests that carry only an API bearer token and no cookie are not browser sessions and do not need CSRF.
2070
+
2071
+ #### HTML on the server
2072
+
2073
+ Anything built into HTML on the server should go through the helpers in `kreftjs/server`:
2074
+
2075
+ ```js
2076
+ const { escapeHtml, cleanHtml, SafeHtml, html } = require('kreftjs/server')
2077
+
2078
+ escapeHtml(userText) // turn text into safe text
2079
+ cleanHtml(userHtml) // keep a small set of tags and attributes, drop the rest
2080
+ ```
2081
+
2082
+ `cleanHtml` keeps an allow-list of tags and attributes, checks every address, and filters `style`. A tag it cannot read is turned into text (`&lt;`), never passed through.
2083
+
2084
+ #### Passwords, keys and tokens
2085
+
2086
+ | Tool | What it does |
2087
+ |---|---|
2088
+ | `Hash.make` / `Hash.check` | Passwords with scrypt, a new salt each time, constant-time compare. |
2089
+ | `Crypt.encryptString` / `Crypt.decryptString` | AES-256-GCM with a fresh IV, keyed by `APP_KEY`. Tampering is detected. |
2090
+ | `Url.signed` / `Url.check` | Links that work only unchanged (and until a time). |
2091
+ | API tokens, reset tokens | Stored as a SHA-256 hash; the plain value is shown once. |
2092
+
2093
+ > [!WARNING]
2094
+ > `APP_KEY` encrypts sessions and signs links. Changing it signs everyone out and breaks old signed links. Keep it out of version control and out of logs.
2095
+
2096
+ #### Limits and lockouts
2097
+
2098
+ - `throttle:60,1` limits requests per visitor and route. The sign-in routes allow 6 a minute.
2099
+ - Accounts lock after repeated wrong passwords or two-factor codes, whatever the address. See [Authentication](#authentication).
2100
+ - Request bodies are limited (`MAX_BODY_KB`), and JSON bodies cannot carry `__proto__` or `constructor` keys.
2101
+ - Every API answer carries `X-Content-Type-Options`, `X-Frame-Options` and `Referrer-Policy`; in production also `Strict-Transport-Security`.
2102
+
2103
+ #### Plain HTTP
2104
+
2105
+ In production a request with `X-Forwarded-Proto: http` is redirected to `https`, and one with `X-Forwarded-Proto: https` is served when it comes from this machine or from a proxy listed in `TRUSTED_PROXIES` (so a proxy that ends TLS works with `FORCE_HTTPS=true` or an `https://` `APP_URL`; anyone else could write that header, so from other addresses it is ignored, and the server says so once in its log). Without that header, `FORCE_HTTPS=true` or an `https://` `APP_URL` redirects every plain request. The address in the redirect comes from `APP_URL` when set, otherwise from the `Host` header only if it looks like a host name; it cannot be used to send visitors to another site.
2106
+
2107
+ #### API tokens
2108
+
2109
+ A token made in the profile page has a list of permissions (`read`, `create`, `update`, `delete`), and the middleware enforces them from the HTTP method on every route. A read-only token cannot delete anything, even if its owner is an admin.
2110
+
2111
+ #### Uploads
2112
+
2113
+ Files get random names. Unknown extensions are stored as `.bin`. Public files are served as plain data with a sandbox policy, so they cannot run scripts. Use `isImage()` to check the real file header.
2114
+
2115
+ #### The database
2116
+
2117
+ - Values travel as parameters. Table and column names are quoted.
2118
+ - The automatic table API (`--db`) is a development tool with no login. `kreftjs serve` only serves the tables you list in `kreftjs.config.json`, with `write: false` and `hidden: [...]` where needed; hidden columns cannot be read or used to sort.
2119
+ - Raw SQL helpers (`whereRaw`, `selectRaw`, `orderByRaw`, `DB.raw`) are for your own SQL; pass user values in their parameter array.
2120
+
2121
+ #### Packages
2122
+
2123
+ `kreftjs add` blocks install scripts, typosquats and look-alike names, flags very new packages, and pins exact versions. See [Packages](#packages).
2124
+
2125
+ ### What stays your job
2126
+
2127
+ - **Check access on the server.** Client route guards decide what to show, not who may read data. Every route that returns private data needs `auth` and, where it matters, `admin` or a policy.
2128
+ - **Never trust HTML you did not clean**, and never put secrets in `PUBLIC_` variables or localStorage.
2129
+ - **Keep dependencies and Node.js up to date** (`kreftjs update`, `npm audit`).
2130
+ - **Protect the machine.** Kreft cannot protect a database, a server or a `.env` file that others can read.
2131
+
2132
+ ## Production checklist
2133
+
2134
+ Go through this before your first real user:
2135
+
2136
+ | Check | Why |
2137
+ |---|---|
2138
+ | `NODE_ENV=production` and `APP_ENV=production` | Turns off debug pages, enables HSTS and the https redirect, and makes security errors fatal. |
2139
+ | `APP_DEBUG=false` | Never show stack traces or source code to visitors. |
2140
+ | `APP_KEY` set to a 32-byte key (`kreftjs key:generate`) | In production the app refuses to start without a key of at least 16 bytes. A random per-process key would also break sessions on restart. |
2141
+ | `APP_URL=https://your.site` | Used for links in emails, the https redirect and secure cookies. |
2142
+ | `TRUSTED_PROXIES` set if you use a proxy | Otherwise every visitor shares the proxy's address and rate limits. |
2143
+ | A real `DATABASE_URL` and a backup plan | SQLite is fine for small apps; use MySQL or PostgreSQL for several servers. |
2144
+ | `CACHE_STORE=redis` or `database`, `QUEUE_CONNECTION=redis` or `database` | `memory` is per process. Account lockouts live in the cache, so they are shared between servers only with `database` or `redis`. |
2145
+ | `MAIL_URL` set | Otherwise emails are only printed in the terminal. |
2146
+ | `MCP_TOKEN` set, or MCP left off | The MCP endpoint is disabled in production without it. |
2147
+ | Tables listed in `kreftjs.config.json`, or no `--db` at all | `kreftjs serve` serves only the tables you list. |
2148
+ | No test user | Do not run `--seed` against production; delete `test@example.com` if it exists. |
2149
+ | HTTPS in front of the app | Cookies are `Secure` in production and on `https` URLs. |
2150
+ | `npm audit` and `kreftjs update` | Fix known vulnerabilities before release. |
2151
+
2152
+ > [!NOTE]
2153
+ > Kreft does not replace a security review. For regulated or high-risk systems, have the app, its infrastructure and its dependencies reviewed by someone who does that professionally.
2154
+
2155
+ ## Known limits
2156
+
2157
+ - Without a build, views are compiled in the browser with `new Function`, which a strict Content-Security-Policy blocks. Use `kreftjs build` for production.
2158
+ - Inside a view, `<` directly followed by a letter starts a tag, so write `{a < b}` with a space.
2159
+ - The memory of which two-factor codes were already used, and login tickets, is kept per process. Behind several servers, a code can be replayed once on each. The per-account lockout (kept in the cache) still limits guessing.
2160
+ - The sanitizer removes unsafe markup; it is not a substitute for choosing carefully what HTML you show.
2161
+ - `--prerender` makes static HTML at build time. On a running server whole pages are drawn only for crawlers and link previews; every other visitor gets the page drawn by the browser, and an address the app does not have answers 200 (the app shows its own 404 page) unless `seo.js` says otherwise.
2162
+ - `.kreftjs` files must be served over `http://` or `https://`, not opened from `file://`.
2163
+
2164
+ # Reference
2165
+
2166
+ ## Environment variables
2167
+
2168
+ Set these in `.env` (never commit it). Variables that start with `PUBLIC_` are copied into the browser as `Kreftjs.env`; never put a secret in one.
2169
+
2170
+ ### App
2171
+
2172
+ | Variable | Meaning |
2173
+ |---|---|
2174
+ | `APP_NAME`, `APP_URL` | Name and public address (used in emails, https redirects, secure cookies). |
2175
+ | `APP_ENV`, `NODE_ENV` | `production` turns on production behaviour (either one is enough). |
2176
+ | `APP_DEBUG` | `true` shows the interactive error page outside production. |
2177
+ | `APP_KEY` | Encrypts sessions and signs links. Make it with `kreftjs key:generate`; required in production. |
2178
+ | `APP_LOCALE`, `APP_FALLBACK_LOCALE`, `APP_TIMEZONE` | Language and time zone defaults. |
2179
+ | `HOST`, `PORT` | Where `kreftjs serve` listens. |
2180
+ | `FORCE_HTTPS` | `true` redirects plain HTTP in production. |
2181
+ | `MAX_BODY_KB` | Largest request body accepted. |
2182
+
2183
+ ### Database, cache and queues
2184
+
2185
+ | Variable | Meaning |
2186
+ |---|---|
2187
+ | `DATABASE_URL` | `sqlite:…`, `mysql://…` or `postgres://…`. |
2188
+ | `DB_CONNECTION`, `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME`, `DB_PASSWORD`, `DB_SSL` | The same as separate values. |
2189
+ | `CACHE_STORE` | `database`, `file`, `memory` or `redis`. Also `CACHE_PREFIX`, `CACHE_DEFAULT_TTL`. |
2190
+ | `QUEUE_CONNECTION` | `database`, `redis` or `sync`. Also `QUEUE_NAMES`, `QUEUE_WORKER`. |
2191
+ | `REDIS_URL`, or `REDIS_HOST` `REDIS_PORT` `REDIS_USERNAME` `REDIS_PASSWORD` `REDIS_DB` `REDIS_TLS` `REDIS_PREFIX` | Redis connection. |
2192
+ | `SCHEDULER` | `off` hands scheduling to cron (`kreftjs schedule:run`). |
2193
+ | `MAX_QUERY_ROWS`, `HARD_QUERY_LIMIT` | Upper limits on rows a query may return. |
2194
+ | `FILESYSTEM_DISK` | `local` or `public`. |
2195
+
2196
+ ### Sign-in and sessions
2197
+
2198
+ | Variable | Default | Meaning |
2199
+ |---|---|---|
2200
+ | `SESSION_LIFETIME` | 120 | Minutes of inactivity before a session ends. |
2201
+ | `SESSION_EXPIRE_ON_CLOSE` | false | End the session when the browser closes. |
2202
+ | `SESSION_SECURE_COOKIE`, `SESSION_SAME_SITE`, `SESSION_DOMAIN`, `SESSION_COOKIE` | production / `Lax` | Cookie attributes. |
2203
+ | `AUTH_TOKEN_DAYS` | 30 | A token unused this long stops working. |
2204
+ | `PASSWORD_TIMEOUT` | 10800 | Seconds a password confirmation stays valid. |
2205
+ | `LOGIN_MAX_FAILURES`, `LOGIN_WINDOW_SECONDS` | 10, 900 | Wrong passwords per account before a lockout. |
2206
+ | `TWO_FACTOR_MAX_FAILURES` | 8 | Wrong two-factor codes per account before a lockout. |
2207
+ | `PUBLIC_SESSION_TIMEOUT`, `PUBLIC_SESSION_WARNING` | 15, 2 | Minutes of page inactivity before sign-out, and the warning before it. |
2208
+
2209
+ ### Network and tools
2210
+
2211
+ | Variable | Meaning |
2212
+ |---|---|
2213
+ | `TRUSTED_PROXIES`, `TRUSTED_PROXY_HOPS` | Which proxies' `X-Forwarded-For` is believed, and how many stand in front with `*`. |
2214
+ | `KREFT_ALLOWED_HOSTS` | Extra host names `kreftjs dev` answers to. |
2215
+ | `MCP_TOKEN`, `MCP_ALLOWED_ORIGIN`, `MCP_QUERY_TIMEOUT_MS` | MCP endpoint token, one extra allowed browser origin, and the query time limit. |
2216
+ | `MAIL_URL`, `MAIL_FROM` | `smtp://user:password@host:587` sends real email (needs `kreftjs add nodemailer`). |
2217
+ | `LOG_LEVEL`, `LOG_CHANNEL` | `debug`, `info`, `notice`, `warning`, `error`, `critical`. |
2218
+ | `AUTO_GC`, `AUTO_GC_THRESHOLD_MB`, `AUTO_GC_INTERVAL_MS` | The memory watchdog. |
2219
+
2220
+ ## API cheat sheet
2221
+
2222
+ | Group | Names |
2223
+ |---|---|
2224
+ | `.kreftjs` keywords | `state` `derive` `action` `when` `watch` `on` `prop` `event` `use` `share` `receive` `import` `saved` `load` `layout` |
2225
+ | In the script | `send` `props` `bus` `store` `$route` `$router` `tick` and every `Kreft` function |
2226
+ | In the view | `{expr}` `attr={expr}` `@event` `k-if` `k-elif` `k-else` `k-each` `k-key` `k-model` `k-show` `k-text` `k-html` `k-ref` `k-skip` `k-once` `k-memo` `k-animate` `{for}` |
2227
+ | App | `Kreftjs.app(rootOrPath)` then `.component()` `.directive()` `.share()` `.use()` `.mount()` `.unmount()` |
2228
+ | Data | `store` (`$patch` `$reset` `$watch`) `state` `signal` `derived` `readonly` `saved` `storage` |
2229
+ | Reacting | `watch` `effect` `tick` `onCleanup` `onReady` `onUpdate` `onShow` `onHide` `onDestroy` |
2230
+ | Sharing | `bus` (`on` `once` `off` `send`) `share` `receive` |
2231
+ | Forms | `form(values, rules)` (`$error` `$errors` `$valid` `$busy` `$submit` `$check` `$reset` `$setErrors`) and `validate(values, rules)` |
2232
+ | Server data | `table(name)` and `api` (`get` `post` `put` `patch` `delete` `setup` `create` `onError`) |
2233
+ | Languages | `$t` `$choice` `$locale` `$dir`, `Kreftjs.locale()` `Kreftjs.translations()` |
2234
+ | Router | `Kreftjs.router()` with `.go` `.replace` `.back` `.before` `.after` `.route`, and `children` for nested routes |
2235
+ | Engine | `Kreftjs.render(view, data, options)` |
2236
+ | Server | `Route` `Model` `DB` `Seeder` `Hash` `Str` `Url` `Mail` `Crypt` `Cache` `Storage` `Schedule` `config` `response` `abort` and the helpers in `require('kreftjs/server')` |
2237
+
2238
+ ## Compared with other frameworks
2239
+
2240
+ Kreft is one framework for the browser and the server. The others below each cover part of that, so the honest comparison is "what do you put next to them". This is a map to help you choose, not a ranking: all of them are good, and most are older, bigger and better known than Kreft.
2241
+
2242
+ | | **Kreft** | **Next.js** | **Nuxt** | **Astro** | **AdonisJS** | **NestJS** |
2243
+ |---|---|---|---|---|---|---|
2244
+ | **What it is** | Frontend and backend in one | React framework | Vue framework | Content sites with islands of interactivity | Backend (MVC) | Backend (modules, dependency injection) |
2245
+ | **Components** | `.kreftjs` files, no Virtual DOM | React | Vue | Astro files; React, Vue, Svelte … inside | none (Edge templates, or Inertia) | none |
2246
+ | **Pages drawn on the server** | At build time (`--prerender`), and for crawlers and link previews on request | Yes, for every visitor | Yes, for every visitor | Yes, mostly at build time | Only with Inertia | no |
2247
+ | **Database layer** | Active Record `Model`, migrations, seeders, factories | none (use Prisma, Drizzle …) | none | none | Lucid (Active Record) | none (TypeORM, Prisma, MikroORM …) |
2248
+ | **Sign-in, 2FA, API tokens** | Built in, in every new project | none (Auth.js …) | none (a module) | none | Built in | none (Passport, your own) |
2249
+ | **Queues, mail, scheduler, cache** | Built in | none | none | none | Built in | Modules |
2250
+ | **AI agent access (MCP)** | Built in | none | none | none | none | none |
2251
+ | **Runtime dependencies** | None | Many | Many | Many | Some | Some |
2252
+ | **Language** | JavaScript or TypeScript | TypeScript first | TypeScript first | TypeScript first | TypeScript first | TypeScript first |
2253
+ | **Ecosystem and hiring** | Small, new | Very large | Large | Large | Medium | Large |
2254
+
2255
+ **Choose Kreft when** you want a whole application (screens, database, sign-in, jobs, mail) from one tool with almost no dependencies, in the Laravel style, and your team is small enough to learn one framework.
2256
+
2257
+ **Choose something else when:**
2258
+
2259
+ - **Next.js or Nuxt:** your team already knows React or Vue, you need the libraries of that ecosystem, or every visitor must get server-drawn HTML with streaming and incremental updates. Kreft draws whole pages on the server only for crawlers and link previews (see "Whole pages for crawlers").
2260
+ - **Astro:** the site is mostly content (blog, documentation, marketing) and ships almost no JavaScript. Kreft apps always ship the framework.
2261
+ - **AdonisJS:** you want the same Laravel-like backend with a larger community and years of production use, and you will bring your own frontend.
2262
+ - **NestJS:** you build a large API or microservices with many teams, and you want strict structure and dependency injection.
2263
+
2264
+ **Things to weigh before choosing Kreft:** it is young and has one small community, few third-party packages are made for `.kreftjs` files, and nobody has run it at large scale yet. See [Known limits](#known-limits).
2265
+
2266
+ ## Coming from Vue
2267
+
2268
+ | Vue | Kreft |
2269
+ |---|---|
2270
+ | `.vue` single-file component | `.kreftjs` file: script, `<view>`, `<style scoped>` |
2271
+ | `ref()`, `reactive()`, `computed()` | `state`, `derive` (JS: `Kreftjs.signal`, `Kreftjs.state`, `Kreftjs.derived`) |
2272
+ | `watch`, `watchEffect` | `watch x { }`, `when cond { }` (JS: `Kreftjs.watch`, `Kreftjs.effect`) |
2273
+ | `{{ x }}`, `:attr`, `@click` | `{x}`, `attr={x}`, `@click={fn}` |
2274
+ | `v-if`, `v-else-if`, `v-else`, `v-show` | `k-if`, `k-elif`, `k-else`, `k-show` |
2275
+ | `v-for` with `:key` | `{for x in list key x.id}` … `{/for}`, or `k-each` + `k-key` |
2276
+ | `v-model`, `v-once`, `v-memo` | `k-model`, `k-once`, `k-memo` |
2277
+ | `defineProps`, `defineEmits`, `emit` | `prop`, `event`, `send()` |
2278
+ | slots, scoped slots | `<slot>`, `<template k-slot:name="{ item }">` |
2279
+ | `provide`, `inject` | `share`, `receive` |
2280
+ | `onMounted`, `onUpdated`, `onUnmounted` | `on ready`, `on update`, `on destroy` |
2281
+ | `<KeepAlive>`, `<Teleport>`, `<Suspense>` | `keep`, `<kreft-portal>`, `<kreft-wait>` |
2282
+ | `<Transition>` | `k-animate="fade"` |
2283
+ | Vue Router with nested routes | `Kreftjs.router()`, `<kreft-view>`, `<kreft-link>`, `children` |
2284
+ | Pinia | `Kreftjs.store()` + `use`, with `{ saved: true }` |
2285
+ | Vite dev server | `kreftjs dev` (or `kreftjs vite:dev`) |
2286
+ | Nuxt static generation | `kreftjs build --prerender` |
2287
+ | Nuxt `server/api` | `server/*.js` served at `/api/*` |
2288
+
2289
+ ## Glossary
2290
+
2291
+ | Term | Meaning |
2292
+ |---|---|
2293
+ | **Component** | A `.kreftjs` file: script, view and style together. |
2294
+ | **State** | A value that can change; the view follows it. |
2295
+ | **Derived value** | A value calculated from other values; stays up to date by itself. |
2296
+ | **Store** | State shared by many components. |
2297
+ | **Layout** | The frame around pages: header, menu, footer. |
2298
+ | **Middleware** | A check that runs before a route (client side) or a controller (server side). |
2299
+ | **Migration** | A file that describes a change to the database schema. |
2300
+ | **Seeder / Factory** | Fills the database with starting data / makes fake rows. |
2301
+ | **Policy** | A class that decides whether a user may do something to a model. |
2302
+ | **MCP** | Model Context Protocol: how AI agents read your project through tools. |