@samebase/convex-hosting 0.9.0 → 0.10.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0
4
+
5
+ - `routeAll(http, component, versionId, worker?)` replaces `serveSite`. It builds the site's HTTP
6
+ action with Convex's `httpActionGeneric` and routes it for every method Convex routes, at every
7
+ path, so `convex/http.ts` reads `routeAll(http, components.hosting, versionId, server)`: no method
8
+ loop and no `httpAction`. It returns the action for the exact routes an app registers past a
9
+ component's HTTP prefix, such as cookie-session routes under Convex Auth's `/auth`. Breaking:
10
+ `serveSite` is gone.
11
+
3
12
  ## 0.9.0
4
13
 
5
14
  - `serveSite(component, versionId, worker?)` returns the handler of the site routes, so
package/README.md CHANGED
@@ -1,358 +1,140 @@
1
1
  # Convex Hosting
2
2
 
3
- Host a web app on Convex, next to its backend: the files of a build and, when the app renders pages,
4
- those pages. The site is at `https://<deployment>.convex.site`.
5
-
6
- It works like
7
- [Cloudflare Workers with static assets](https://developers.cloudflare.com/workers/static-assets/):
8
- the same `_headers`, `_redirects`, and `.assetsignore` files, the same `html_handling` and
9
- `not_found_handling` options, and the same responses, computed by Cloudflare's own asset-serving
10
- code, checked against Cloudflare's own test cases for `html_handling` and path encoding, and against
11
- its integration tests for a files-only site, run on a local deployment.
12
-
13
- ## Setup
3
+ Serves a web app from its own Convex deployment, at `https://<deployment>.convex.site`: the files of
4
+ a build, and the pages the app renders.
5
+
6
+ The serving code is Cloudflare's. The asset Worker behind
7
+ [Cloudflare Workers with static assets](https://developers.cloudflare.com/workers/static-assets/) is
8
+ copied from [cloudflare/workers-sdk](https://github.com/cloudflare/workers-sdk) at a pinned commit
9
+ and runs inside a Convex HTTP action. So `_headers`, `_redirects`, `.assetsignore`, `html_handling`,
10
+ and `not_found_handling` mean what they mean on Cloudflare, and a request gets the response a Worker
11
+ sends. This repository's CI runs Cloudflare's own tests against it: its
12
+ [196 cases](https://github.com/samebase/convex-hosting/blob/main/packages/convex-hosting/src/component/conformance.test.ts)
13
+ for `html_handling` and path encoding, and its
14
+ [integration test](https://github.com/samebase/convex-hosting/tree/main/packages/convex-hosting/src/workers-sdk/fixtures/workers-with-assets-only)
15
+ for a files-only site, unchanged, on a local Convex deployment.
16
+
17
+ An app that renders pages takes the place of the Worker script: an object with `fetch(request)` that
18
+ answers what no file answers. TanStack Start has
19
+ [an adapter](https://github.com/samebase/convex-hosting/tree/main/packages/convex-hosting-tanstack-start),
20
+ with streamed pages, server functions, server routes, request middleware, and cookie sessions. The
21
+ [TanStack Start example](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-ssr)
22
+ is deployed here and, from the same source, as a real Worker; CI keeps the two byte-identical but
23
+ for the files that name the host.
24
+
25
+ Use it when the backend is Convex and a second host for the frontend is one host too many. A Worker
26
+ has what Convex does not: the data center nearest the visitor, Node's built-in modules, an edge
27
+ cache, and WebSockets. The list, with numbers, is in
28
+ [What differs from a Worker](https://github.com/samebase/convex-hosting/blob/main/docs/workers.md).
29
+
30
+ ## Use
14
31
 
15
32
  Requires `convex` 1.46 or later.
16
33
 
17
- 1. Install:
18
-
19
- ```sh
20
- npm install @samebase/convex-hosting
21
- ```
22
-
23
- 2. `convex/convex.config.ts` mounts the component:
24
-
25
- ```ts
26
- import { defineApp } from "convex/server";
27
- import hosting from "@samebase/convex-hosting/convex.config";
28
-
29
- const app = defineApp();
30
- app.use(hosting);
31
-
32
- export default app;
33
- ```
34
-
35
- 3. `convex/http.ts` sends every method to `serveSite`, as every request reaches a Cloudflare Worker:
36
- a file answers GET, another method on a file answers 405, and Convex answers `HEAD` through the
37
- GET handler.
38
-
39
- ```ts
40
- import { httpRouter } from "convex/server";
41
- import { serveSite } from "@samebase/convex-hosting";
42
- import { components } from "./_generated/api.js";
43
- import { httpAction } from "./_generated/server.js";
44
- import { versionId } from "../.convex-hosting/version.js";
45
-
46
- const http = httpRouter();
47
-
48
- const site = httpAction(serveSite(components.hosting, versionId));
49
- for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] as const) {
50
- http.route({ pathPrefix: "/", method, handler: site });
51
- }
52
-
53
- export default http;
54
- ```
55
-
56
- 4. Deploy once before the first `npx convex dev` push. `convex/http.ts` imports
57
- `.convex-hosting/version.js`, which only a deploy writes, and the deploy's build needs the
58
- deployment's URL, which only `npx convex dev` writes to `.env.local`. So on a fresh clone:
59
-
60
- ```sh
61
- npx convex dev --skip-push # creates your dev deployment and .env.local, no push
62
- npx convex-hosting deploy --dev --build "vite build" --dist dist --not-found-handling single-page-application
63
- npx convex dev # then work as usual
64
- ```
65
-
66
- The `deploy --dev` line builds, uploads, writes `.convex-hosting/version.js`, and pushes. If
67
- `.convex-hosting/` is deleted later, `npx convex dev` fails with
68
- `Could not resolve "../.convex-hosting/version.js"`, and `deploy --dev` fixes it.
69
-
70
- 5. Deploy production, from CI or a terminal:
71
-
72
- ```sh
73
- npx convex-hosting deploy --build "vite build" --dist dist --not-found-handling single-page-application
74
- ```
75
-
76
- An app set up this way: the
77
- [SPA example](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-spa), live at
78
- [quick-fish-630.convex.site](https://quick-fish-630.convex.site).
79
-
80
- `convex-hosting` keeps its state in `.convex-hosting/` at the project root, next to `convex/`, like
81
- Wrangler's `.wrangler/`. The folder has its own `.gitignore`, so git ignores it. The package
82
- declares the type of `.convex-hosting/version.js`, so the app typechecks without it. A
83
- server-rendered app gets the type of its server build from the framework adapter.
34
+ ```sh
35
+ npm install @samebase/convex-hosting
36
+ ```
84
37
 
85
- ## Rules
38
+ `convex/convex.config.ts` mounts the component:
86
39
 
87
- - Deploy production and previews with `convex-hosting deploy`, never with a plain
88
- `npx convex deploy`. A plain push sends whatever version `.convex-hosting/version.js` names, the
89
- version of this checkout's last deploy, which is often another deployment's: the site answers 404
90
- until the next `convex-hosting deploy`.
91
- - Do not commit `.convex-hosting/`. It ignores itself.
92
- - Do not change directory in `--build`. The build and the upload run in the project directory.
40
+ ```ts
41
+ import { defineApp } from "convex/server";
42
+ import hosting from "@samebase/convex-hosting/convex.config";
93
43
 
94
- ## Deploy
44
+ const app = defineApp();
45
+ app.use(hosting);
95
46
 
96
- `deploy` runs `npx convex deploy` with your build and the upload of `--dist` as its `--cmd`. The
97
- pushed code names the version it was built with, so the push switches the site. Then the command
98
- prints the site URL, which it asks the deployment for (`CONVEX_SITE_URL`), and ends. The first
99
- request that the new code serves marks its version live, which deletes the versions that were live
100
- before it. A version that never becomes live, from a deploy that failed, is deleted when it is a day
101
- old, with the stored files that no version uses. Nothing is deleted while a site gets no request.
47
+ export default app;
48
+ ```
102
49
 
103
- `deploy` goes to production. `--preview-name <name>` sends it to that preview (with a login or a
104
- project's preview deploy key; always pass the name with that key), and `--dev` to the development
105
- deployment (see [Development](#development)). A deploy key in `CONVEX_DEPLOY_KEY` or
106
- `CONVEX_DEPLOYMENT_TOKEN` (from the environment or `.env.local`) selects its own deployment instead,
107
- and so does `CONVEX_SELF_HOSTED_URL` for a self-hosted deployment.
50
+ `convex/http.ts` registers the site, every method at every path, as every request reaches a
51
+ Cloudflare Worker:
108
52
 
109
- The first deploy to a deployment pushes the component, then uploads the files. Until that upload
110
- ends, the site answers 404.
53
+ ```ts
54
+ import { httpRouter } from "convex/server";
55
+ import { routeAll } from "@samebase/convex-hosting";
56
+ import { components } from "./_generated/api.js";
57
+ import { versionId } from "../.convex-hosting/version.js";
111
58
 
112
- Run one deploy at a time per deployment; the examples'
113
- [deploy workflow](https://github.com/samebase/convex-hosting/blob/main/.github/workflows/deploy-examples.yml)
114
- queues them, with one `concurrency` group per deployment.
59
+ const http = httpRouter();
115
60
 
116
- ## Deploy tools and CI
117
-
118
- - **The deploy keys** go into repository secrets. Create the production key with the Convex CLI and
119
- hand it to `gh secret set` on standard input, which keeps it off the screen and out of process
120
- arguments:
121
-
122
- ```sh
123
- key=$(npx convex deployment token create "<repository> deploy.yml" --prod) && printf '%s' "$key" | gh secret set CONVEX_DEPLOY_KEY
124
- ```
125
-
126
- The CLI cannot create a project's preview deploy key, so use the management API, with a personal
127
- or team access token from the Convex dashboard in `CONVEX_TOKEN`:
128
-
129
- ```sh
130
- key=$(curl -s -X POST "https://api.convex.dev/v1/projects/<project id>/create_preview_deploy_key" \
131
- -H "Authorization: Bearer $CONVEX_TOKEN" -H "Content-Type: application/json" \
132
- -d '{"name": "<repository> preview.yml"}' | jq -er .previewDeployKey) && printf '%s' "$key" | gh secret set CONVEX_PREVIEW_DEPLOY_KEY
133
- ```
134
-
135
- A failed first command stops before anything is stored.
136
-
137
- - **A tool that runs `convex deploy` itself**, such as Alchemy's `Convex.Code`, has no place for the
138
- upload, which must happen between the build and the push. Run `convex-hosting deploy` in place of
139
- that tool's push step, with the deploy key in `CONVEX_DEPLOY_KEY`.
140
- - **A deploy key limited to some actions** needs `deployment:deploy`,
141
- `deployment:functions:runInternalQueries`, `deployment:functions:runInternalMutations`,
142
- `deployment:functions:runInternalActions` (the upload calls one internal action), and
143
- `deployment:functions:runTestQuery` (the first-install check and the site URL are inline queries).
144
- - **Checks that must stop a release** run before `convex-hosting deploy`, because its push switches
145
- the site. Checks that need the new functions run after it.
146
- - **A re-run of an old workflow run** checks out its old commit, and its push puts older code over
147
- newer code; a `concurrency` group orders runs but does not refuse a stale one. So the examples'
148
- workflows check, right before the deploy, that the commit is still the head of `main`, or of the
149
- pull request for a preview, and fail otherwise.
150
- - **A step that needs the site URL**, such as a preview comment, reads it from the deploy step: when
151
- `GITHUB_OUTPUT` is set, `deploy` appends `url=<site URL>` to it, so give the deploy step an `id`
152
- and read `steps.<id>.outputs.url`.
153
-
154
- ## Your own domain
155
-
156
- A custom domain needs the Convex Pro plan. Add it to the deployment in the dashboard, under
157
- Deployment Settings, Custom Domains, with "HTTP Actions" as the destination, or with the management
158
- API: `POST https://api.convex.dev/v1/deployments/<deployment>/create_custom_domain` with
159
- `{"domain": "<domain>", "requestDestination": "convexSite"}`.
160
-
161
- Then create the two records that the dashboard shows: a `CNAME` from the domain to `convex.domains`,
162
- and a `TXT` record at `_convex_domains.<domain>` whose value is the deployment's name. On
163
- Cloudflare, make the `CNAME` DNS only; Convex documents a proxied record as an
164
- [advanced setup](https://docs.convex.dev/production/abuse-protection). Verification can take up to
165
- 30 minutes.
166
-
167
- Do not choose the domain under "Override Environment Variables". That changes `CONVEX_SITE_URL`
168
- everywhere in the app, and Convex Auth uses it as the token issuer and in its redirect URLs.
169
-
170
- An apex domain, such as `example.com`, relies on Cloudflare's CNAME flattening and is not tested
171
- yet.
172
-
173
- ## Development
174
-
175
- The inner loop is your framework's dev server, such as Vite's, which serves the frontend locally
176
- with hot reload against the development deployment's URL from `.env.local`, plus `npx convex dev`,
177
- which pushes your function changes. Neither needs a deploy.
178
-
179
- When the development deployment should serve the current build, run the one-shot command:
61
+ routeAll(http, components.hosting, versionId);
180
62
 
181
- ```sh
182
- npx convex-hosting deploy --dev --build "vite build" --dist dist --not-found-handling single-page-application
63
+ export default http;
183
64
  ```
184
65
 
185
- `deploy --dev` goes to the development deployment of this checkout, the one in `.env.local`. It runs
186
- the build itself, uploads `--dist`, writes `.convex-hosting/version.js`, and pushes with
187
- `npx convex run --push`, which works whether or not `npx convex dev` runs. The build runs with your
188
- environment, so the framework reads the deployment's URL from `.env.local` (Vite reads
189
- `VITE_CONVEX_URL`). Against an anonymous local deployment, `deploy --dev` needs `npx convex dev`
190
- running, because without it every Convex CLI call starts and stops its own backend and the upload's
191
- requests find nothing; cloud development deployments do not have this constraint. There is no watch
192
- mode: a build, an upload, and a push per change is not an inner loop, and Cloudflare's own tooling
193
- works the same way, with a local dev server and a push on demand.
194
-
195
- Two cases are transient:
196
-
197
- - A server-rendered app imports its server build in `convex/http.ts`, so a running `npx convex dev`
198
- pushes after the build, before `deploy --dev` writes the new version. Until `deploy --dev` ends,
199
- the new pages refer to files that answer 404.
200
- - A production deploy writes production's version to the version file, and a running
201
- `npx convex dev` pushes it to the development deployment, where that version does not exist, until
202
- the next `deploy --dev`. Run production deploys in CI or in a separate checkout.
203
-
204
- ## Options
205
-
206
- | Option | Values |
207
- | ------------------------------ | -------------------------------------------------------------------------------------- |
208
- | `--dev` | The development deployment of this checkout, instead of production |
209
- | `--preview-name <name>` | The preview deployment with this name, instead of production |
210
- | `--html-handling <value>` | `auto-trailing-slash` (default), `force-trailing-slash`, `drop-trailing-slash`, `none` |
211
- | `--not-found-handling <value>` | `none` (default), `single-page-application`, `404-page` |
212
- | `--component <name>` | The component instance, `hosting` by default |
213
-
214
- The two handling options work as
215
- [Cloudflare documents them](https://developers.cloudflare.com/workers/static-assets/routing/).
216
-
217
- Each file of `--dist` must be 25 MiB or smaller, and the CLI names a file that is larger. This is
218
- Cloudflare's limit per asset, and the HTTP action that serves a file cannot answer more than
219
- Convex's 100 MiB response limit anyway.
220
-
221
- ## `_headers`, `_redirects`, and `.assetsignore`
222
-
223
- Put them at the root of `--dist`, as for Cloudflare:
224
- [`_headers`](https://developers.cloudflare.com/workers/static-assets/headers/),
225
- [`_redirects`](https://developers.cloudflare.com/workers/static-assets/redirects/), and
226
- [`.assetsignore`](https://developers.cloudflare.com/workers/static-assets/binding/#ignoring-assets).
227
- For a Vite app, put them in `public/`, which Vite copies into `dist/`. They are not served. Symbolic
228
- links in `--dist` are followed, as Wrangler follows them, also when they point outside the folder; a
229
- link to nothing, or to a folder that contains it, is skipped with a message, and a link to another
230
- folder in `--dist` uploads its files under both paths. `.assetsignore` excludes what a link should
231
- not publish.
232
-
233
- Every asset has `Cache-Control: public, max-age=0, must-revalidate` and an `ETag`. To cache hashed
234
- files for a year, add a `_headers` rule:
66
+ Deploy:
235
67
 
236
- ```
237
- /assets/*
238
- Cache-Control: public, max-age=31536000, immutable
68
+ ```sh
69
+ npx convex-hosting deploy --build "vite build" --dist dist --not-found-handling single-page-application
239
70
  ```
240
71
 
241
- ## Routing
242
-
243
- `serveSite(component, versionId, worker?)` returns the handler of the site routes. It answers with
244
- the file that matches, else with what `worker` answers (see
245
- [Server-rendered pages](#server-rendered-pages)), else 404. Another method than GET or HEAD on a
246
- path that has a file answers 405, as on Cloudflare; on any other path it goes to `worker`, else 404.
247
- `worker` is the Worker script, an object with `fetch(request)`: the code that runs on a miss.
248
- Everything it returns gets `Cache-Control: no-store`, and so does a 404, because the `*.convex.site`
249
- front caches paths with an extension. The front keeps `no-store` for a path without an extension and
250
- replaces it with a four-hour browser `max-age` for a path with one, so a browser that asks for a
251
- script before its upload ended keeps the 404; the deploy order prevents that case, because no page
252
- refers to a file that is not uploaded yet. When the version does not exist or is still uploading, it
253
- answers 404 naming the version. At the first request for a new version, it marks the version live.
254
-
255
- As on Cloudflare, the presence of code decides where `not_found_handling` (the single-page fallback
256
- or the 404 page) applies:
257
-
258
- - Without `worker`, the site is files only, like a Worker without a script: `not_found_handling`
259
- applies to every request. `curl`, uptime monitors, and link-preview bots get what a browser gets.
260
- - With `worker`, your code is the Worker: `not_found_handling` applies to navigation requests
261
- (`Sec-Fetch-Mode: navigate`) only, and other requests that match no file reach your code. Browsers
262
- send that header whenever they open a page. `curl`, `fetch` in scripts and tests, uptime monitors,
263
- and link-preview bots do not. To test a deep link as a browser opens it, send the header:
264
- `curl -H "Sec-Fetch-Mode: navigate"`, or `node:http` in Node, whose `fetch` refuses to set it.
265
-
266
- On a files-only single-page app, a missing hashed file such as `/assets/old.js` gets the shell, with
267
- 200 and `text/html`, as on Cloudflare. A `_headers` rule for its path applies to it too, so with the
268
- one-year rule above a browser keeps that HTML for a year; without a rule, the deployment's
269
- Cloudflare front gives a path with an extension a four-hour browser `max-age`. Hashed names do not
270
- come back.
271
-
272
- One difference from Cloudflare follows from Convex's router: an exact route in `convex/http.ts` wins
273
- over an asset at the same path.
274
-
275
- ## Open tabs after a deploy
276
-
277
- A page opened before a deploy keeps asking for its hashed files, which the new version does not
278
- have, as on Cloudflare: they answer 404, or the shell on a files-only single-page app. To reload
279
- open tabs on a new version, add a query that returns the bundled version:
72
+ It builds, uploads `dist`, pushes the functions, and prints
73
+ `Deployed version <id> to https://<deployment>.convex.site.` Deploy production with this command,
74
+ never with a plain `npx convex deploy`: a plain push uploads no files, and the site answers 404
75
+ until the next `convex-hosting deploy`.
280
76
 
281
- ```ts
282
- // convex/siteVersion.ts
283
- import { v } from "convex/values";
284
- import { query } from "./_generated/server.js";
285
- import { versionId } from "../.convex-hosting/version.js";
286
-
287
- export const get = query({ args: {}, returns: v.string(), handler: () => versionId });
288
- ```
77
+ On a fresh clone, `npx convex dev` fails until the first deploy: `convex/http.ts` imports
78
+ `.convex-hosting/version.js`, which only a deploy writes, and the deploy's build needs the
79
+ deployment URL, which only `npx convex dev` writes to `.env.local`. So the first time:
289
80
 
290
- and reload when its value changes after the first one:
291
-
292
- ```tsx
293
- import { useQuery } from "convex/react";
294
- import { useEffect, useRef } from "react";
295
- import { api } from "../convex/_generated/api";
296
-
297
- export function useReloadOnNewVersion() {
298
- const versionId = useQuery(api.siteVersion.get);
299
- const first = useRef<string | undefined>(undefined);
300
- useEffect(() => {
301
- if (versionId === undefined) return;
302
- first.current ??= versionId;
303
- if (versionId !== first.current) location.reload();
304
- }, [versionId]);
305
- }
81
+ ```sh
82
+ npx convex dev --skip-push # creates your dev deployment and .env.local, no push
83
+ npx convex-hosting deploy --dev --build "vite build" --dist dist --not-found-handling single-page-application
84
+ npx convex dev # then work as usual
306
85
  ```
307
86
 
308
- ## Server-rendered pages
309
-
310
- `serveSite` takes a third argument, `worker`, the object with `fetch(request)` that the app's server
311
- entry exports, and calls it when no file matches. `convex/http.ts` imports the server build
312
- directly, such as `../dist/server/server.js` for TanStack Start, so `npx convex dev` bundles it only
313
- after a build: on a fresh clone, the first `deploy --dev` builds before the first push (see
314
- [Setup](#setup)).
315
- [`@samebase/convex-hosting-tanstack-start`](https://github.com/samebase/convex-hosting/tree/main/packages/convex-hosting-tanstack-start)
316
- renders TanStack Start pages in it. See the
317
- [TanStack Start example](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-ssr),
318
- live at [gallant-llama-40.convex.site](https://gallant-llama-40.convex.site).
319
-
320
- ## The same app on Cloudflare Workers
321
-
322
- [`examples/tanstack-ssr-workers`](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-ssr-workers)
323
- is the TanStack Start example deployed as a Cloudflare Worker with static assets, with Convex still
324
- the backend. Live at
325
- [convex-hosting-tanstack-ssr-workers.pfp.workers.dev](https://convex-hosting-tanstack-ssr-workers.pfp.workers.dev).
326
- [`examples/tanstack-ssr-workers.patch`](https://github.com/samebase/convex-hosting/blob/main/examples/tanstack-ssr-workers.patch)
327
- is the diff between the two folders, and so the definition of what convex-hosting adds to an app. CI
328
- writes the patch again from the committed folders and fails when it differs from the committed file;
329
- `pnpm run examples:diff` refreshes it. The patch covers:
330
-
331
- - `convex/http.ts`: on Convex only. It serves the files and forwards every other request to
332
- `src/server.ts`.
333
- - `src/server.ts`: the same file on both hosts, the `/auth` routes and the session lines around the
334
- server entry, but for two lines: the entry it wraps (the adapter's on Convex, TanStack Start's
335
- default on the Worker) and `no-store` on rendered pages, which `serveSite` sets on Convex.
336
- - `convex/convex.config.ts` and `convex/_generated/api.d.ts`: `app.use(hosting)` on Convex only.
337
- - `vite.config.ts`: `convexSsr()` on Convex; Cloudflare's Vite plugin on the Worker.
338
- - `package.json`: `@samebase/convex-hosting` and `convex-hosting deploy` on Convex; `wrangler`,
339
- `@cloudflare/vite-plugin`, and the Workers Builds scripts on the Worker.
340
- - `wrangler.jsonc`: on the Worker only.
341
- - `README.md`.
342
-
343
- Workers Builds deploys it. Once, in the Cloudflare dashboard, create a Worker from this repository
344
- with:
345
-
346
- - Root directory: `examples/tanstack-ssr-workers`.
347
- - Build command: `pnpm run build`. Deploy command: `pnpm run deploy`. Builds for non-production
348
- branches on, with the preview command `pnpm run deploy:preview`.
349
- - Build variable `CONVEX_DEPLOY_KEY`: a production deploy key of the example's own Convex project in
350
- the production build variables, and the project's preview deploy key in Previews Base.
351
- - Build variables `NODE_VERSION` `24.21.0` and `PNPM_VERSION` `12.10.1`, the versions this
352
- repository pins, in both. The Builds image defaults to Node 22.16.0, below the 22.18 that Vite+
353
- needs, and to pnpm 10.11.1.
354
-
355
- The example's README describes the Convex side: the signing keys of production and of previews.
87
+ For pages rendered on the server, pass the app's server entry, the object with `fetch(request)`, as
88
+ the fourth argument of `routeAll`. The
89
+ [adapter](https://github.com/samebase/convex-hosting/tree/main/packages/convex-hosting-tanstack-start)
90
+ shows it for TanStack Start.
91
+
92
+ ## How a deploy switches the site
93
+
94
+ On Cloudflare, `wrangler deploy` uploads the files and the Worker code as one version, so the switch
95
+ is atomic. Here two systems hold the two halves: the component's storage holds the files, and
96
+ `npx convex deploy` pushes the code. `deploy` binds them the same way. It uploads the files as a new
97
+ version, writes that version's id to `.convex-hosting/version.js`, and then pushes the code, which
98
+ imports the file and asks the component for exactly that version. The push is the switch: no pointer
99
+ moves in a second step, so there is no moment when new code serves old files, and no page refers to
100
+ a file that is not uploaded yet. The first request the new code serves marks its version live and
101
+ deletes the versions that were live before it. A version whose push never came is deleted at a later
102
+ switch, once it is a day old; nothing is deleted by the clock alone. The one rule: run one deploy at
103
+ a time per deployment. The
104
+ [design](https://github.com/samebase/convex-hosting/blob/main/docs/design.md#orderings-and-timing)
105
+ works through every interleaving.
106
+
107
+ ## Examples
108
+
109
+ - [tanstack-spa](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-spa): a
110
+ single-page app, live at [quick-fish-630.convex.site](https://quick-fish-630.convex.site).
111
+ - [tanstack-ssr](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-ssr): pages
112
+ rendered per request, with cookie sessions, live at
113
+ [gallant-llama-40.convex.site](https://gallant-llama-40.convex.site). Its
114
+ [Worker twin](https://github.com/samebase/convex-hosting/tree/main/examples/tanstack-ssr-workers)
115
+ is live at
116
+ [convex-hosting-tanstack-ssr-workers.pfp.workers.dev](https://convex-hosting-tanstack-ssr-workers.pfp.workers.dev).
117
+
118
+ ## Documentation
119
+
120
+ - [Deploy](https://github.com/samebase/convex-hosting/blob/main/docs/deploy.md): what `deploy` does,
121
+ previews, deploy keys, CI, and the options.
122
+ - [Development](https://github.com/samebase/convex-hosting/blob/main/docs/development.md): the inner
123
+ loop, `deploy --dev`, and reloading open tabs after a deploy.
124
+ - [Serving](https://github.com/samebase/convex-hosting/blob/main/docs/serving.md): `_headers`,
125
+ `_redirects`, `.assetsignore`, caching, routing, and server-rendered pages.
126
+ - [Your own domain](https://github.com/samebase/convex-hosting/blob/main/docs/domains.md).
127
+ - [What differs from a Worker](https://github.com/samebase/convex-hosting/blob/main/docs/workers.md),
128
+ and the same app deployed as one.
129
+ - [Design](https://github.com/samebase/convex-hosting/blob/main/docs/design.md): why each piece is
130
+ what it is.
131
+ - [Changelog](https://github.com/samebase/convex-hosting/blob/main/packages/convex-hosting/CHANGELOG.md).
132
+
133
+ ## Status
134
+
135
+ Version 0.9, before 1.0: a minor release can change the API, and the changelog says what changed.
136
+ The author's apps run on it, among them [Scout](https://github.com/samebase/scout) and
137
+ [chessmerge.com](https://chessmerge.com).
356
138
 
357
139
  ## License
358
140
 
@@ -1,18 +1,14 @@
1
- import type { GenericActionCtx, GenericDataModel } from "convex/server";
1
+ import { type HttpRouter, type PublicHttpAction } from "convex/server";
2
2
  import type { ComponentApi } from "../component/_generated/component.js";
3
3
  /**
4
- * The handler of the app's site routes, for every method: it answers a request with the file of
5
- * version `versionId`, else with what `worker` answers, else 404. `worker` is the Worker script,
6
- * the object with `fetch(request)` that the app's server entry exports: the code that runs on a
7
- * miss. As on Cloudflare, the presence of code decides how `not_found_handling` applies: with
8
- * `worker`, it applies to navigations only and `worker` answers the other misses; without it, the
9
- * site is files only, so it applies to every request.
4
+ * Routes the site on the app's router: `serveSite`'s action for every method Convex routes, at
5
+ * every path, as every request reaches a Cloudflare Worker with static assets. Convex answers HEAD
6
+ * through GET.
10
7
  *
11
- * A page and a 404 answer `Cache-Control: no-store`. The deployment's Cloudflare front keeps that
12
- * for a path without an extension and replaces it with a four-hour browser `max-age` for a path
13
- * with one, so a browser that asks for a script before its upload ended keeps the 404; the deploy
14
- * order prevents that case, because no page refers to a file that is not uploaded yet.
8
+ * Returns the action, for the paths that a component's HTTP prefix would otherwise take: Convex
9
+ * picks an exact route first, then the longest prefix, so an app whose session routes sit under a
10
+ * component's prefix registers them with `http.route({ path, method, handler })`.
15
11
  */
16
- export declare function serveSite(component: ComponentApi, versionId: string, worker?: {
12
+ export declare function routeAll(http: HttpRouter, component: ComponentApi, versionId: string, worker?: {
17
13
  fetch(request: Request): Promise<Response>;
18
- }): (ctx: Pick<GenericActionCtx<GenericDataModel>, "runQuery" | "runMutation">, request: Request) => Promise<Response>;
14
+ }): PublicHttpAction;
@@ -1,18 +1,20 @@
1
+ import { httpActionGeneric, ROUTABLE_HTTP_METHODS, } from "convex/server";
1
2
  /**
2
- * The handler of the app's site routes, for every method: it answers a request with the file of
3
- * version `versionId`, else with what `worker` answers, else 404. `worker` is the Worker script,
4
- * the object with `fetch(request)` that the app's server entry exports: the code that runs on a
5
- * miss. As on Cloudflare, the presence of code decides how `not_found_handling` applies: with
6
- * `worker`, it applies to navigations only and `worker` answers the other misses; without it, the
7
- * site is files only, so it applies to every request.
3
+ * The HTTP action that serves the site, for `routeAll`: it answers a request with the file of
4
+ * version `versionId` (another method than GET or HEAD on a file answers 405), else with what
5
+ * `worker` answers, else 404. `worker` is the Worker script, the object with `fetch(request)`
6
+ * that the app's server entry exports: the code that runs on a miss. As on Cloudflare, the presence
7
+ * of code decides how `not_found_handling` applies: with `worker`, it applies to navigations only
8
+ * and `worker` answers the other misses; without it, the site is files only, so it applies to every
9
+ * request.
8
10
  *
9
11
  * A page and a 404 answer `Cache-Control: no-store`. The deployment's Cloudflare front keeps that
10
12
  * for a path without an extension and replaces it with a four-hour browser `max-age` for a path
11
13
  * with one, so a browser that asks for a script before its upload ended keeps the 404; the deploy
12
14
  * order prevents that case, because no page refers to a file that is not uploaded yet.
13
15
  */
14
- export function serveSite(component, versionId, worker) {
15
- return async (ctx, request) => {
16
+ function serveSite(component, versionId, worker) {
17
+ return httpActionGeneric(async (ctx, request) => {
16
18
  const asset = await answer(ctx, component, request, versionId, worker !== undefined);
17
19
  if (asset !== null)
18
20
  return asset;
@@ -23,7 +25,23 @@ export function serveSite(component, versionId, worker) {
23
25
  const headers = new Headers(page.headers);
24
26
  headers.set("Cache-Control", "no-store");
25
27
  return new Response(page.body, { status: page.status, headers });
26
- };
28
+ });
29
+ }
30
+ /**
31
+ * Routes the site on the app's router: `serveSite`'s action for every method Convex routes, at
32
+ * every path, as every request reaches a Cloudflare Worker with static assets. Convex answers HEAD
33
+ * through GET.
34
+ *
35
+ * Returns the action, for the paths that a component's HTTP prefix would otherwise take: Convex
36
+ * picks an exact route first, then the longest prefix, so an app whose session routes sit under a
37
+ * component's prefix registers them with `http.route({ path, method, handler })`.
38
+ */
39
+ export function routeAll(http, component, versionId, worker) {
40
+ const site = serveSite(component, versionId, worker);
41
+ for (const method of ROUTABLE_HTTP_METHODS) {
42
+ http.route({ pathPrefix: "/", method, handler: site });
43
+ }
44
+ return site;
27
45
  }
28
46
  /**
29
47
  * Answers `request` with the asset of version `versionId` that Cloudflare's asset handler picks,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@samebase/convex-hosting",
3
- "version": "0.9.0",
4
- "description": "A Convex component that hosts a web app: static files, with optional server rendering.",
3
+ "version": "0.10.0",
4
+ "description": "Serves a web app from its own Convex deployment: the files of a build, and the pages the app renders.",
5
5
  "keywords": [
6
6
  "component",
7
7
  "convex",