@orieljs/framework 0.1.0-alpha.1 → 0.1.0-alpha.11

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 (88) hide show
  1. package/NOTICE +7 -0
  2. package/README.md +10 -1
  3. package/TRADEMARKS.md +33 -0
  4. package/dist/{chunk-WI7E7Q46.js → chunk-24673JQF.js} +8 -4
  5. package/dist/chunk-24673JQF.js.map +1 -0
  6. package/dist/{chunk-K6MAA3RM.js → chunk-HTJKT2R2.js} +9 -1
  7. package/dist/chunk-HTJKT2R2.js.map +1 -0
  8. package/dist/chunk-KDE5BLI3.js +8776 -0
  9. package/dist/chunk-KDE5BLI3.js.map +1 -0
  10. package/dist/chunk-MURTCTRH.js +3253 -0
  11. package/dist/chunk-MURTCTRH.js.map +1 -0
  12. package/dist/contracts.d.ts +852 -168
  13. package/dist/contracts.js +9 -1
  14. package/dist/index.d-WQpfdIbG.d.ts +630 -0
  15. package/dist/index.d.ts +3015 -5160
  16. package/dist/index.js +7628 -2640
  17. package/dist/index.js.map +1 -1
  18. package/dist/{jsx-runtime-B8J6xq-7.d.ts → jsx-runtime-6um4Mwfb.d.ts} +99 -149
  19. package/dist/jsx-runtime.d.ts +3 -1
  20. package/dist/jsx-runtime.js +3 -3
  21. package/dist/testing-CKwfWJGn.d.ts +1586 -0
  22. package/dist/testing.d.ts +3 -1408
  23. package/dist/testing.js +29 -5
  24. package/docs/authentication.md +487 -0
  25. package/docs/authorization.md +435 -0
  26. package/docs/cache.md +647 -0
  27. package/docs/collections.md +2270 -0
  28. package/docs/configuration.md +334 -0
  29. package/docs/console.md +654 -0
  30. package/docs/container.md +430 -0
  31. package/docs/controllers.md +406 -0
  32. package/docs/csrf.md +126 -0
  33. package/docs/database-testing.md +144 -0
  34. package/docs/database.md +236 -0
  35. package/docs/deployment.md +371 -0
  36. package/docs/eloquent-collections.md +218 -0
  37. package/docs/eloquent-factories.md +255 -0
  38. package/docs/eloquent-mutators.md +254 -0
  39. package/docs/eloquent-observers.md +145 -0
  40. package/docs/eloquent-relationships.md +557 -0
  41. package/docs/eloquent.md +494 -0
  42. package/docs/encryption.md +104 -0
  43. package/docs/errors.md +318 -0
  44. package/docs/events.md +336 -0
  45. package/docs/export.md +93 -0
  46. package/docs/facades.md +278 -0
  47. package/docs/hashing.md +118 -0
  48. package/docs/helpers.md +1281 -0
  49. package/docs/http-tests.md +386 -0
  50. package/docs/inertia.md +485 -0
  51. package/docs/installation.md +219 -0
  52. package/docs/lifecycle.md +234 -0
  53. package/docs/live-tests.md +226 -0
  54. package/docs/live.md +910 -0
  55. package/docs/logging.md +255 -0
  56. package/docs/mail.md +619 -0
  57. package/docs/middleware.md +385 -0
  58. package/docs/migrations.md +416 -0
  59. package/docs/mocking.md +264 -0
  60. package/docs/navigation.json +277 -0
  61. package/docs/notifications.md +225 -0
  62. package/docs/pagination.md +247 -0
  63. package/docs/passwords.md +207 -0
  64. package/docs/providers.md +187 -0
  65. package/docs/queries.md +600 -0
  66. package/docs/queues.md +330 -0
  67. package/docs/requests.md +533 -0
  68. package/docs/responses.md +444 -0
  69. package/docs/routing.md +738 -0
  70. package/docs/scheduling.md +277 -0
  71. package/docs/seeding.md +122 -0
  72. package/docs/session.md +409 -0
  73. package/docs/starter-kits.md +399 -0
  74. package/docs/storage.md +192 -0
  75. package/docs/structure.md +264 -0
  76. package/docs/testing.md +165 -0
  77. package/docs/time-travel.md +138 -0
  78. package/docs/validation.md +1792 -0
  79. package/docs/verification.md +152 -0
  80. package/docs/views.md +566 -0
  81. package/docs/vite.md +400 -0
  82. package/package.json +37 -30
  83. package/dist/chunk-AI2RFMBT.js +0 -745
  84. package/dist/chunk-AI2RFMBT.js.map +0 -1
  85. package/dist/chunk-BZPCRKFQ.js +0 -2941
  86. package/dist/chunk-BZPCRKFQ.js.map +0 -1
  87. package/dist/chunk-K6MAA3RM.js.map +0 -1
  88. package/dist/chunk-WI7E7Q46.js.map +0 -1
@@ -0,0 +1,236 @@
1
+ # Database: getting started
2
+
3
+ Your app talks to SQLite, PostgreSQL or MySQL through one API: raw SQL with the `DB` facade, the query builder, and
4
+ your models.
5
+
6
+ ## Introduction
7
+
8
+ Oriel reaches your database through [Kysely](https://kysely.dev), with three drivers:
9
+
10
+ - **SQLite**, with `better-sqlite3` on Node and Bun's own `bun:sqlite` on Bun. A new app uses it, so you need no
11
+ database server to start.
12
+ - **PostgreSQL**, with `pg`.
13
+ - **MySQL**, with `mysql2`.
14
+
15
+ `pg` and `mysql2` aren't installed with the framework: install the one you need, `npm install pg` or
16
+ `npm install mysql2`. A driver is loaded the first time your app queries the database.
17
+
18
+ ### Configuration
19
+
20
+ Your connections are in `config/database.ts`. `default` names the one your app uses, `sqlite` unless
21
+ `DB_CONNECTION` names another:
22
+
23
+ ```ts
24
+ declare const basePath: string
25
+ declare function join(...paths: string[]): string
26
+ // ---cut---
27
+ const database = {
28
+ default: env('DB_CONNECTION', 'sqlite'),
29
+ connections: {
30
+ sqlite: { driver: 'sqlite', database: env('DB_DATABASE', join(basePath, 'database/database.sqlite')) },
31
+ pgsql: { driver: 'pgsql', host: env('DB_HOST', '127.0.0.1'), port: Number(env('DB_PORT', 5432)), database: env('DB_DATABASE', 'oriel') },
32
+ },
33
+ }
34
+ ```
35
+
36
+ Each connection takes `host`, `port`, `database`, `username` and `password`, read from the `DB_*` variables of your
37
+ `.env` (see [Databases and migrations](installation.md#databases-and-migrations)). `pool.max` is the size of the
38
+ connection pool, 10 unless you set it. A `pgsql` connection takes a `search_path`, and a `mysql` one a `charset`,
39
+ `utf8mb4` unless you set it.
40
+
41
+ #### SQLite configuration
42
+
43
+ A SQLite connection's `database` is the path of the database file. A new app's is `database/database.sqlite`, as an
44
+ absolute path, since a relative one is opened from the folder your app runs in; `migrate` creates the file. `:memory:`
45
+ gives a database in memory, which your tests use.
46
+
47
+ SQLite checks foreign keys, as `foreign_key_constraints: true` asks; set it to `false` to turn the checks off. Changing
48
+ a table on SQLite rebuilds it: [SQLite and schema changes](migrations.md#sqlite-and-schema-changes) explains.
49
+
50
+ #### Configuration using URLs
51
+
52
+ A `pgsql` or `mysql` connection can take a single `url`, such as the one a hosting provider gives you, in `DB_URL`:
53
+
54
+ ```dotenv
55
+ DB_URL=postgresql://user:secret@db.example.com:5432/blog
56
+ ```
57
+
58
+ A URL's parts win over the separate keys. A SQLite connection ignores `url`: give it a `database` path.
59
+
60
+ > [!NOTE]
61
+ > Laravel's read and write connections, its `sqlsrv` driver and a connection's table `prefix` aren't in Oriel. A new
62
+ > app's `config/database.ts` keeps a `prefix: ''` line from Laravel's file, which nothing reads.
63
+
64
+ ## Running SQL queries
65
+
66
+ The `DB` facade runs raw SQL on the default connection. Every method is async.
67
+
68
+ #### Running a select query
69
+
70
+ `DB.select()` runs a `select`, with the values for its `?` placeholders in an array, and returns the rows as objects:
71
+
72
+ ```ts title="app/Http/Controllers/UserController.ts"
73
+ import { DB } from '@orieljs/framework'
74
+ import { Controller } from './Controller'
75
+
76
+ export class UserController extends Controller {
77
+ /** Show a list of all of the application's users. */
78
+ async index() {
79
+ const users = await DB.select<{ id: number; name: string }>('select * from users where active = ?', [1])
80
+
81
+ return { users }
82
+ }
83
+ }
84
+ ```
85
+
86
+ The values are bound, never written into the SQL, so they are safe from SQL injection. The type you give the rows is
87
+ a cast, not a check.
88
+
89
+ > [!NOTE]
90
+ > Laravel also takes named bindings (`:id`). Oriel takes `?` only, and finds them by splitting the SQL at each `?`,
91
+ > so a `?` inside a string, or PostgreSQL's `?` operator, counts as one too: pass such text as a binding.
92
+
93
+ #### Using multiple database connections
94
+
95
+ `DB.connection()` returns another connection of `config/database.ts`, with `select()`, `statement()`,
96
+ `affectingStatement()`, `table()` and `transaction()`:
97
+
98
+ ```ts
99
+ const users = await DB.connection('sqlite').select('select * from users')
100
+ ```
101
+
102
+ `DB.usingConnection('sqlite', () => ...)` runs a function with another connection as the default, and puts the default
103
+ back after it.
104
+
105
+ #### Using Kysely
106
+
107
+ Oriel's queries run on Kysely. `DB.kysely()` returns the connection's Kysely instance, for a query the query builder
108
+ doesn't write; inside a transaction it is the transaction's, so its queries join it:
109
+
110
+ ```ts
111
+ const rows = await DB.kysely().selectFrom('users').select(['id', 'name']).where('votes', '>', 100).execute()
112
+ ```
113
+
114
+ #### Running an insert statement
115
+
116
+ ```ts
117
+ await DB.insert('insert into users (id, name) values (?, ?)', [1, 'Marc'])
118
+ ```
119
+
120
+ #### Running an update statement
121
+
122
+ `DB.update()` returns the number of rows it changed:
123
+
124
+ ```ts
125
+ const affected = await DB.update('update users set votes = 100 where name = ?', ['Anita'])
126
+ ```
127
+
128
+ #### Running a delete statement
129
+
130
+ `DB.delete()` returns the number of rows it deleted:
131
+
132
+ ```ts
133
+ const deleted = await DB.delete('delete from users')
134
+ ```
135
+
136
+ #### Running a general statement
137
+
138
+ `DB.statement()` runs SQL that returns nothing, such as a schema change, and `DB.affectingStatement()` returns the
139
+ number of rows a statement changed:
140
+
141
+ ```ts
142
+ await DB.statement('drop table users')
143
+ ```
144
+
145
+ #### Raw expressions
146
+
147
+ `DB.raw()` makes an expression the query builder writes as it is, with its own bindings:
148
+
149
+ ```ts
150
+ const users = await DB.table('users').select(DB.raw('count(*) as user_count, status')).groupBy('status').get()
151
+ ```
152
+
153
+ A raw expression is SQL you write, so never put a user's input in it: bind it with `?` instead.
154
+
155
+ ### Values in bindings
156
+
157
+ Oriel turns some values into what the database stores: `true` and `false` become `1` and `0` (except on PostgreSQL,
158
+ which has booleans), a `Date` becomes its UTC time as `'2026-01-02 03:04:05'`, an object or an array becomes JSON,
159
+ and `undefined` becomes `null`.
160
+
161
+ Dates come back as strings, on every driver: your models' date casts turn them into `Date` objects.
162
+
163
+ ## Database transactions
164
+
165
+ `DB.transaction()` runs a function in a transaction. When the function throws, the transaction is rolled back and the
166
+ error is thrown again; otherwise it is committed:
167
+
168
+ ```ts
169
+ await DB.transaction(async () => {
170
+ await DB.update('update users set votes = 1')
171
+
172
+ await DB.delete('delete from posts')
173
+ })
174
+ ```
175
+
176
+ The queries your function runs, through the facade, the query builder or your models, are in the transaction, and
177
+ those of other requests running at the same time aren't. A transaction inside another uses a savepoint, so an inner
178
+ one that throws rolls back only its own work. The function's result is `transaction()`'s:
179
+
180
+ ```ts
181
+ const user = await DB.transaction(async () => {
182
+ const id = await DB.table('users').insertGetId({ name: 'Taylor', email: 'taylor@example.com', password: '' })
183
+
184
+ return DB.table('users').where('id', id).first()
185
+ })
186
+ ```
187
+
188
+ > [!NOTE]
189
+ > Laravel's `transaction()` takes a number of attempts for deadlocks as its second argument. Oriel's doesn't: catch
190
+ > the error and run it again yourself.
191
+
192
+ ### Manually using transactions
193
+
194
+ `DB.beginTransaction()` starts a transaction that lasts, for every query on the connection, until `DB.commit()` or
195
+ `DB.rollBack()`:
196
+
197
+ ```ts
198
+ await DB.beginTransaction()
199
+
200
+ try {
201
+ await DB.update('update users set votes = 1')
202
+ await DB.commit()
203
+ } catch (error) {
204
+ await DB.rollBack()
205
+ throw error
206
+ }
207
+ ```
208
+
209
+ Prefer `transaction()`: a manual transaction takes in the queries of every request until it ends, which suits a
210
+ script or a test, not a request.
211
+
212
+ ## Logging queries
213
+
214
+ `DB.enableQueryLog()` records each query the default connection runs, with its bindings and how many milliseconds it
215
+ took, and `DB.getQueryLog()` returns them:
216
+
217
+ ```ts
218
+ DB.enableQueryLog()
219
+
220
+ await DB.table('users').get()
221
+
222
+ const queries = DB.getQueryLog() // [{ query: 'select * from "users"', bindings: [], time: 0.4 }]
223
+ ```
224
+
225
+ A query that fails throws a `QueryError`, whose message ends with the connection's name and the SQL, and which has
226
+ `sql` and `bindings` fields.
227
+
228
+ > [!NOTE]
229
+ > Laravel's `DB::listen()` and its cumulative query time warnings, the `db` command and the `db:show` and `db:table`
230
+ > commands aren't in Oriel.
231
+
232
+ ## Closing connections
233
+
234
+ Your app's server, its `oriel` commands and its queue workers close their database connections when they shut down. A
235
+ script of your own can end with `await app.shutdown()`, or close them with `await DB.disconnectAll()`;
236
+ `DB.disconnect('mysql')` closes one.
@@ -0,0 +1,371 @@
1
+ # Deployment
2
+
3
+ To run your app in production, build its assets, set its environment, and keep its server, queue workers and
4
+ scheduler running under a process manager.
5
+
6
+ ## Introduction
7
+
8
+ An Oriel app is a long-running process: one server answers every request, where a PHP app starts afresh for each one.
9
+ So deploying it means starting that process, keeping it running, and restarting it with each new version, along with
10
+ the processes beside it: the queue workers, the scheduler and, for the Inertia kits with server-side rendering, the
11
+ SSR server.
12
+
13
+ An app whose pages are the same for every visitor can skip the server altogether: [Static export](export.md) saves its
14
+ pages as files any static host serves. On Vercel, your app runs without a server of its own, in a function that starts
15
+ when requests come: see [Deploying to Vercel](#deploying-to-vercel).
16
+
17
+ ## Server requirements
18
+
19
+ Your server needs **Node 22** or later, or **Bun 1.2** or later. On Node, your app's TypeScript runs through `tsx`,
20
+ which every app has as a dependency; Bun runs it as it is. There is no compile step for your server code.
21
+
22
+ A PostgreSQL or MySQL database needs its driver in your dependencies, `pg` or `mysql2`. SQLite needs nothing more.
23
+
24
+ ## Building your application
25
+
26
+ On each deploy, install the dependencies from your lockfile, build the assets, and run the migrations:
27
+
28
+ ```sh
29
+ npm ci
30
+ npm run build
31
+ node oriel migrate --force
32
+ ```
33
+
34
+ `npm run build` writes your pages' scripts and styles into `public/build`, and, in the Inertia kits with server-side
35
+ rendering, the SSR bundle into `bootstrap/ssr`. The API kit has no `build` script: skip that step. `migrate` asks
36
+ for `--force` in production, since it changes your database.
37
+
38
+ Leave `public/hot` off your server: Vite writes it while `npm run dev` runs, and while it exists, your pages load their
39
+ assets from a Vite dev server rather than from `public/build`. Git ignores it, so a deploy from your repository
40
+ doesn't have it.
41
+
42
+ ## Configuring the environment
43
+
44
+ Give your server its own values, in its environment or in its own `.env` (see [Configuration](configuration.md)). At
45
+ least:
46
+
47
+ ```dotenv
48
+ APP_ENV=production
49
+ APP_DEBUG=false
50
+ APP_KEY=base64:...
51
+ APP_URL=https://example.com
52
+ ```
53
+
54
+ `APP_KEY` must be the same on every server of the app, and stay the same from one deploy to the next: it decrypts
55
+ your cookies and sessions, and checks your signed URLs. Generate it once, with `node oriel key:generate --show`.
56
+
57
+ In production, `migrate` and the other commands that change your data need `--force`, `key:generate` won't replace a
58
+ key that is set, `migrate` doesn't rewrite `types/models.d.ts`, and the kits' dashboard hides its Getting started
59
+ checklist.
60
+
61
+ ## Server configuration
62
+
63
+ ### Running the server
64
+
65
+ `public/index.ts` is your app's production entry point. It creates your app and serves it:
66
+
67
+ ```sh
68
+ node --import tsx public/index.ts
69
+ ```
70
+
71
+ On Bun, run `bun public/index.ts`. The server listens on `127.0.0.1`, on the port the `PORT` variable names, or
72
+ `SERVER_PORT`, or 8000. It serves with `node:http` on Node and `Bun.serve()` on Bun.
73
+
74
+ `node oriel serve` runs the same server through your app's command line, and takes `--host` and `--port`; `npm start`
75
+ runs it too, and so does the CI workflow the installer writes. Leave `--watch` out in production: it restarts the
76
+ server on every file change.
77
+
78
+ ### Containers
79
+
80
+ In a container, the server must listen on every interface, not only on `127.0.0.1`, or nothing outside the container
81
+ reaches it. With `node oriel serve`, pass `--host=0.0.0.0`:
82
+
83
+ ```sh
84
+ node oriel serve --host=0.0.0.0 --port=8000
85
+ ```
86
+
87
+ With `public/index.ts`, give `serve()` the host:
88
+
89
+ ```ts title="public/index.ts"
90
+ // @filename: bootstrap/app.ts
91
+ import type { Application } from '@orieljs/framework'
92
+ export declare function createApplication(): Promise<Application>
93
+ // ---cut---
94
+ import { serve } from '@orieljs/framework'
95
+ import { createApplication } from '../bootstrap/app'
96
+
97
+ // Production entry: `node --import tsx public/index.ts`, or `bun public/index.ts`.
98
+ await serve(await createApplication(), { hostname: '0.0.0.0' })
99
+ ```
100
+
101
+ `serve()` takes these options:
102
+
103
+ | Option | What it does |
104
+ | --- | --- |
105
+ | `hostname` | The address to listen on (default: `127.0.0.1`) |
106
+ | `port` | The port (default: `PORT`, then `SERVER_PORT`, then 8000; 0 picks a free one) |
107
+ | `publicPath` | The folder whose files are sent as they are (default: `public`); `false` turns it off |
108
+ | `shutdownTimeout` | How long a shutdown waits for the requests in flight, in milliseconds (default: 10000) |
109
+ | `signals` | Whether SIGINT and SIGTERM shut the server down (default: `true`) |
110
+
111
+ Point your container's health check at the [health route](#the-health-route), `/up`.
112
+
113
+ ### Static files
114
+
115
+ The server sends a file that exists in `public` before your routes run, so `public/build` and your icons need no
116
+ other web server. The built assets, under `/build/assets/`, are sent with a year-long, immutable cache header, as
117
+ their names change with their content. TypeScript files and dotfiles in `public` are never sent.
118
+
119
+ A web server or a CDN in front of your app can serve `public` itself; then pass `publicPath: false` to `serve()`.
120
+
121
+ ### Behind a proxy
122
+
123
+ Behind a load balancer or a proxy that ends TLS, your app sees the proxy's request: plain HTTP, from the proxy's
124
+ address. List your proxies in `http.trusted_proxies`, in `config/http.ts`, so that `request().ip()`, `url()` and
125
+ `route()` use the client's address, scheme and host from the `X-Forwarded-*` headers.
126
+ [Configuring trusted proxies](requests.md#configuring-trusted-proxies) shows the settings.
127
+
128
+ ### Directory permissions
129
+
130
+ Your app writes into `storage`: its logs, uploaded files, and the file cache and sessions when you use those drivers.
131
+ The user that runs it needs write access there, and in `database` when you use SQLite.
132
+
133
+ ### Shutting down
134
+
135
+ On SIGTERM or SIGINT, the server stops taking new connections and lets the requests in flight finish, for up to 10
136
+ seconds (`shutdownTimeout`). Then the app shuts down: it closes the database pools and the Redis connections that the
137
+ cache, the queue and the session opened, and writes out its logs. Then the process exits with code 0. A process
138
+ manager that sends SIGTERM to stop or restart your app gets a clean exit, with no log line lost and no request cut
139
+ short, unless it takes longer than 10 seconds. When closing the handles takes more than 10 seconds, the rest are given
140
+ up on, so the process always exits.
141
+
142
+ The `oriel` commands shut down the same way when they finish, and a queue worker finishes its current job first.
143
+
144
+ ## Optimization
145
+
146
+ > [!NOTE]
147
+ > Laravel caches its configuration, routes, events and views before it serves (`optimize`, `config:cache`,
148
+ > `route:cache`, `event:cache`, `view:cache`), since each PHP request would read them again. Oriel's process reads
149
+ > them once, when it starts, and keeps them: there is nothing to cache, and none of those commands exists.
150
+
151
+ ## Queue workers
152
+
153
+ A queue worker is a process of its own: `node oriel queue:work` takes jobs off the queue until it is stopped (see
154
+ [Running the queue worker](queues.md#running-the-queue-worker)). Keep your workers running with a process manager,
155
+ such as Supervisor or systemd, which starts a new worker whenever one exits.
156
+
157
+ A worker stops by itself in a few cases, so that the process manager starts it again with a clean slate:
158
+
159
+ - `--memory`: when its JavaScript heap passes this many megabytes, 512 unless you set it (exit code 12).
160
+ - `--max-jobs` and `--max-time`: after that many jobs, or seconds (exit code 0).
161
+ - A job that runs past its timeout (exit code 1).
162
+ - `queue:restart` (exit code 0, after the current job).
163
+
164
+ Since a worker also exits with code 0, the process manager must restart it on every exit, not only on a failure. A
165
+ Supervisor program for two workers:
166
+
167
+ ```ini
168
+ [program:blog-worker]
169
+ process_name=%(program_name)s_%(process_num)02d
170
+ command=node oriel queue:work --sleep=3 --tries=3 --memory=512 --max-time=3600
171
+ directory=/var/www/blog
172
+ autostart=true
173
+ autorestart=true
174
+ stopasgroup=true
175
+ killasgroup=true
176
+ user=www-data
177
+ numprocs=2
178
+ redirect_stderr=true
179
+ stdout_logfile=/var/www/blog/storage/logs/worker.log
180
+ stopwaitsecs=3600
181
+ ```
182
+
183
+ `stopwaitsecs` gives a worker time to finish its current job when Supervisor stops it: set it longer than your
184
+ longest job.
185
+
186
+ ### Restarting the workers
187
+
188
+ A worker loads your code once, so after a deploy it still runs the old code. Tell every worker to stop after its
189
+ current job:
190
+
191
+ ```sh
192
+ node oriel queue:restart
193
+ ```
194
+
195
+ The process manager then starts each one again, on the new code. `queue:restart` leaves its signal in your app's
196
+ default cache store, which the workers read after each job, so the workers and the command must share that store:
197
+ the `database` store of a new app does, as long as they share the database. With the `array` store, a worker never
198
+ sees the signal. While the cache can't be read, a worker goes on working.
199
+
200
+ ## Task scheduling
201
+
202
+ The scheduler runs the tasks you schedule in `routes/console.ts`. Run `schedule:run` every minute with cron, and it
203
+ runs the tasks that are due:
204
+
205
+ ```sh
206
+ * * * * * cd /var/www/blog && node oriel schedule:run >> /dev/null 2>&1
207
+ ```
208
+
209
+ Where you have no cron, as in many containers, run `node oriel schedule:work` under your process manager instead: it
210
+ runs the due tasks every minute until it is stopped.
211
+
212
+ ## The SSR server
213
+
214
+ The React, Vue, Svelte and Solid kits render their pages on the server through a separate SSR server. Keep it running
215
+ beside your app, under your process manager:
216
+
217
+ ```sh
218
+ node oriel inertia:start-ssr
219
+ ```
220
+
221
+ On Bun, add `--runtime=bun`. It runs the bundle that `npm run build` wrote into `bootstrap/ssr`, on the port of
222
+ `INERTIA_SSR_URL` (`http://127.0.0.1:13714` unless you set it), and exits when the bundle does. When no SSR server
223
+ answers, your pages are rendered in the browser instead, so a restart doesn't break them.
224
+
225
+ After a deploy, stop it, and let the process manager start it on the new bundle:
226
+
227
+ ```sh
228
+ node oriel inertia:stop-ssr
229
+ ```
230
+
231
+ The Angular kit, the Live kit and the API kit have no SSR server.
232
+
233
+ ## Reloading services
234
+
235
+ After a deploy, every long-running process must restart to run the new code: your server, your queue workers and
236
+ your SSR server. Restart the server through your process manager, then:
237
+
238
+ ```sh
239
+ node oriel queue:restart
240
+ node oriel inertia:stop-ssr
241
+ ```
242
+
243
+ Laravel's `reload` command, which does both, isn't in Oriel.
244
+
245
+ ## Deploying to Vercel
246
+
247
+ On Vercel there is no server to keep running: Vercel's CDN sends the files in `public`, and every other request goes
248
+ to a function that runs your app. `node oriel vercel:build` writes both into `.vercel/output`, the folder Vercel
249
+ deploys (its [Build Output API](https://vercel.com/docs/build-output-api)): `static` holds `public`, whose built
250
+ assets are cached for a year as `serve()` sends them, and the function holds your app's code,
251
+ `public/build/manifest.json` and your production dependencies. Your `.env` files and your
252
+ SQLite databases are left out: the secrets come from Vercel's environment variables, and the database from a server.
253
+
254
+ ### Setting up the project
255
+
256
+ Add a `vercel.json` to your app, so that Vercel's build runs the command:
257
+
258
+ ```json title="vercel.json"
259
+ {
260
+ "framework": null,
261
+ "buildCommand": "npm run build && node oriel migrate --force && node oriel vercel:build"
262
+ }
263
+ ```
264
+
265
+ The API kit has no `build` script: leave `npm run build &&` out. Then import your repository in Vercel, or deploy from
266
+ your machine with `npx vercel deploy`: either way, the build runs on Vercel's machines, which run Linux, as its
267
+ functions do. The function holds native packages built for the machine that ran the build, such as esbuild, which
268
+ `tsx` needs, so a function built on macOS or Windows fails on Vercel. `vercel:build` warns when it runs elsewhere than
269
+ Linux: don't deploy that build with `vercel deploy --prebuilt`.
270
+
271
+ `migrate --force` runs on every deploy, the previews' too. Give the Preview environment a database of its own, such as
272
+ a branch of your production database, so that a preview never migrates your live data.
273
+
274
+ ### Environment variables
275
+
276
+ Set your app's values in the project's settings, under Environment Variables:
277
+
278
+ ```dotenv
279
+ APP_ENV=production
280
+ APP_DEBUG=false
281
+ APP_KEY=base64:...
282
+ APP_URL=https://example.vercel.app
283
+ DB_CONNECTION=pgsql
284
+ DB_URL=postgres://...
285
+ SESSION_DRIVER=database
286
+ CACHE_STORE=database
287
+ QUEUE_CONNECTION=sync
288
+ INERTIA_SSR_ENABLED=false
289
+ ```
290
+
291
+ Vercel gives them to the build, which runs the migrations, and to the function. PostgreSQL needs `pg` in your
292
+ dependencies (`npm install pg`); Vercel's Marketplace has PostgreSQL databases with a free plan, which set `DB_URL`'s
293
+ value for you under a name of their own, such as `DATABASE_URL`: copy it into `DB_URL`. When the address says
294
+ `sslmode=require`, as Neon's do, `pg` warns on every cold start: write `sslmode=verify-full`, which also checks the
295
+ server's certificate.
296
+
297
+ ### What changes on Vercel
298
+
299
+ Vercel starts an instance of the function when requests come, and stops it when they stop. The first request to a new
300
+ instance boots your app; the next ones reuse it. Nothing your app keeps in memory or on disk outlasts the instance,
301
+ and each instance has its own, so:
302
+
303
+ - **Your database is a server**, PostgreSQL or MySQL. The function's files are read-only, so SQLite can't write.
304
+ - **Sessions and the cache** need a store every instance shares: `database` (the kits have the `sessions` and `cache`
305
+ tables' migrations) or `redis`. Sessions can also use `cookie`. `file` and `array` don't outlast the instance.
306
+ - **Logs** go to stderr, which Vercel's logs show: the function sets `LOG_CHANNEL=stderr` when you don't set it.
307
+ - **Vercel's proxy is trusted.** Every request reaches the function through it, and it sets the `X-Forwarded-*`
308
+ headers itself, so the function trusts them when `trusted_proxies`, in `config/http.ts`, is empty: your URLs keep
309
+ the browser's `https`, and `request().ip()` is the visitor's (see
310
+ [Configuring trusted proxies](requests.md#configuring-trusted-proxies)).
311
+ - **Files your app stores** go on an `s3` disk (see [File storage](storage.md)): the `local` and `public` disks write
312
+ into `storage`, which the function can't.
313
+ - **No queue worker or scheduler runs.** With `QUEUE_CONNECTION=sync`, a job runs during the request that dispatches
314
+ it.
315
+ - **Inertia pages render in the browser**, as they do when no SSR server answers: the SSR server doesn't run on
316
+ Vercel. `INERTIA_SSR_ENABLED=false` skips asking it.
317
+
318
+ ### Caching pages on the CDN
319
+
320
+ Pages that are the same for every visitor can be kept by Vercel's CDN, so that most requests for them never reach the
321
+ function. The `cache.headers` middleware sets their `Cache-Control`: `s_maxage` is how many seconds the CDN keeps a
322
+ page, and `stale_while_revalidate` how long it may still send the old copy while it fetches a new one:
323
+
324
+ ```ts title="routes/web.ts"
325
+ import { Route, StartSession, VerifyCsrfToken, view } from '@orieljs/framework'
326
+
327
+ Route.get('/pricing', () => view('pricing'))
328
+ .middleware('cache.headers:public;s_maxage=3600;stale_while_revalidate=60')
329
+ .withoutMiddleware(StartSession, VerifyCsrfToken)
330
+ ```
331
+
332
+ `cache.headers` leaves a response that sets a cookie alone, so that the CDN never hands one visitor's cookies to the
333
+ next. The `web` group's `StartSession` and `VerifyCsrfToken` set cookies on every page: take them off the pages you
334
+ cache, as above. Those pages have no session, so they can't show who is signed in.
335
+
336
+ ## Debug mode
337
+
338
+ `debug` in `config/app.ts` reads `APP_DEBUG`, and decides how much an error response shows (see
339
+ [Error handling](errors.md#configuration)).
340
+
341
+ > [!WARNING]
342
+ > In production, `APP_DEBUG` must be `false`. The debug page shows your source code and your error messages, which
343
+ > can hold secrets, to anyone who triggers an error.
344
+
345
+ ## The health route
346
+
347
+ A new app answers `/up` with a small page and a 200 when it is running, for your uptime monitor, your load
348
+ balancer or your container's health check. The route is outside the `web` middleware group, so it starts no session.
349
+ `health` in `withRouting()`, in `bootstrap/app.ts`, sets its URI:
350
+
351
+ ```ts
352
+ Application.configure({ basePath: '.' }).withRouting({ web: 'routes/web.ts', console: 'routes/console.ts', health: '/status' })
353
+ ```
354
+
355
+ Each request to it dispatches the `DiagnosingHealth` event. A listener can check what your app needs, such as its
356
+ database, and throw when it fails:
357
+
358
+ ```ts title="app/Providers/AppServiceProvider.ts"
359
+ import { DB, DiagnosingHealth, Event, ServiceProvider } from '@orieljs/framework'
360
+
361
+ export class AppServiceProvider extends ServiceProvider {
362
+ override boot(): void {
363
+ Event.listen(DiagnosingHealth, async () => {
364
+ await DB.select('select 1')
365
+ })
366
+ }
367
+ }
368
+ ```
369
+
370
+ When a listener throws, the route answers 500, "Application experiencing problems", and the error is reported. With
371
+ `APP_DEBUG=true`, the error page shows the error instead.