diff --git a/examples/tanstack-ssr-workers.patch b/examples/tanstack-ssr-workers.patch
index 35d99464..56be1234 100644
--- a/examples/tanstack-ssr-workers.patch
+++ b/examples/tanstack-ssr-workers.patch
@@ -282,7 +282,7 @@ index f712af5c5a73e65f8a9e64cd486a68078218b4ea..7bbec71850aefea2dd2b0d87cfd9ca6f
}
}
diff --git a/src/server.ts b/src/server.ts
-index 98c671d10f422e183216c3c6ac33b9d3ff07e712..200022cafe2f901c7cee6a6c92dc89f7a241e25c 100644
+index 276e876364b7ec56cc28b6a2a57229c2835d681e..73deca587d2c5dc5a35d40e073f2b6ed737cec38 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,5 +1,5 @@
@@ -293,7 +293,7 @@ index 98c671d10f422e183216c3c6ac33b9d3ff07e712..200022cafe2f901c7cee6a6c92dc89f7
import { api } from "../convex/_generated/api.js";
diff --git a/vite.config.ts b/vite.config.ts
-index c1fa9a2d4c1224a8cbda8132e633b7b066e137b4..3e6badb4024aed63c5e612137eb9fc19df6cbb9c 100644
+index 8e68d2fff175c09d037e869b6661cae7188aaa51..3e156c689f99f1e5ada6dd2e38c10494338c9504 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,12 +1,12 @@
@@ -309,8 +309,8 @@ index c1fa9a2d4c1224a8cbda8132e633b7b066e137b4..3e6badb4024aed63c5e612137eb9fc19
- convexSsr(),
+ cloudflare({ viteEnvironment: { name: "ssr" } }),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
- // route, and crawling would follow /about's links.
- tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
+ // route, and crawling would follow /about's links. The header marks the prerender's requests
+ // for src/server.ts; a prerender renders without a visitor.
diff --git a/wrangler.jsonc b/wrangler.jsonc
new file mode 100644
index 0000000000000000000000000000000000000000..48c90a9e2b5789638c5baf349fe0cd88dcbbb282
diff --git a/examples/tanstack-ssr-workers/src/routes/__root.tsx b/examples/tanstack-ssr-workers/src/routes/__root.tsx
index 6efdbfd4..536f1379 100644
--- a/examples/tanstack-ssr-workers/src/routes/__root.tsx
+++ b/examples/tanstack-ssr-workers/src/routes/__root.tsx
@@ -9,6 +9,8 @@ import { useQuery, type QueryClient } from "@tanstack/react-query";
import { convexQuery, type ConvexQueryClient } from "@convex-dev/react-query";
import {
SessionProvider,
+ SignedIn,
+ SignedOut,
sessionBeforeLoad,
useSignOutSession,
} from "@samebase/convex-hosting-tanstack-start/convex-auth";
@@ -69,13 +71,28 @@ function Root() {
}
function SessionHeader() {
- // `useQuery` does not fetch on the server, so the header of a prerendered page stays empty until
- // the browser has the answer.
+ // The server render knows the session from its token, so a rendered page carries the right
+ // header; a prerendered page has no token and shows its header once the browser knows.
+ return (
+ <>
+
+ Sign in
+
+
+
+
+ >
+ );
+}
+
+function SignedInHeader() {
+ // The root loader has queried the user for a page rendered as the user, so the name is in the
+ // cache; `useQuery` does not fetch on the server, so a prerendered page shows it when the browser
+ // has it.
const { data: me } = useQuery(convexQuery(api.users.me, {}));
const signOut = useSignOutSession("/auth/signout");
const [signOutFailed, setSignOutFailed] = useState(false);
- if (me === undefined) return null;
- if (me === null) return Sign in;
+ if (!me) return null;
return (
<>
{`Signed in as ${me.name}`}
diff --git a/examples/tanstack-ssr-workers/src/server.ts b/examples/tanstack-ssr-workers/src/server.ts
index 200022ca..73deca58 100644
--- a/examples/tanstack-ssr-workers/src/server.ts
+++ b/examples/tanstack-ssr-workers/src/server.ts
@@ -40,7 +40,9 @@ const server = {
cookies,
cookieOptions,
});
- const token = await session.getToken();
+ // The build's prerender renders without a visitor, so its page, which every visitor gets, must
+ // not say signed out: vite.config.ts marks its requests, and they get no token at all.
+ const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
const headers = new Headers(page.headers);
cookies.applyTo(headers);
diff --git a/examples/tanstack-ssr-workers/vite.config.ts b/examples/tanstack-ssr-workers/vite.config.ts
index 3e6badb4..3e156c68 100644
--- a/examples/tanstack-ssr-workers/vite.config.ts
+++ b/examples/tanstack-ssr-workers/vite.config.ts
@@ -8,8 +8,12 @@ export default defineConfig({
plugins: [
cloudflare({ viteEnvironment: { name: "ssr" } }),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
- // route, and crawling would follow /about's links.
- tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
+ // route, and crawling would follow /about's links. The header marks the prerender's requests
+ // for src/server.ts; a prerender renders without a visitor.
+ tanstackStart({
+ prerender: { headers: { "x-prerender": "1" } },
+ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }],
+ }),
react(),
],
});
diff --git a/examples/tanstack-ssr/src/routes/__root.tsx b/examples/tanstack-ssr/src/routes/__root.tsx
index 6efdbfd4..536f1379 100644
--- a/examples/tanstack-ssr/src/routes/__root.tsx
+++ b/examples/tanstack-ssr/src/routes/__root.tsx
@@ -9,6 +9,8 @@ import { useQuery, type QueryClient } from "@tanstack/react-query";
import { convexQuery, type ConvexQueryClient } from "@convex-dev/react-query";
import {
SessionProvider,
+ SignedIn,
+ SignedOut,
sessionBeforeLoad,
useSignOutSession,
} from "@samebase/convex-hosting-tanstack-start/convex-auth";
@@ -69,13 +71,28 @@ function Root() {
}
function SessionHeader() {
- // `useQuery` does not fetch on the server, so the header of a prerendered page stays empty until
- // the browser has the answer.
+ // The server render knows the session from its token, so a rendered page carries the right
+ // header; a prerendered page has no token and shows its header once the browser knows.
+ return (
+ <>
+
+ Sign in
+
+
+
+
+ >
+ );
+}
+
+function SignedInHeader() {
+ // The root loader has queried the user for a page rendered as the user, so the name is in the
+ // cache; `useQuery` does not fetch on the server, so a prerendered page shows it when the browser
+ // has it.
const { data: me } = useQuery(convexQuery(api.users.me, {}));
const signOut = useSignOutSession("/auth/signout");
const [signOutFailed, setSignOutFailed] = useState(false);
- if (me === undefined) return null;
- if (me === null) return Sign in;
+ if (!me) return null;
return (
<>
{`Signed in as ${me.name}`}
diff --git a/examples/tanstack-ssr/src/server.ts b/examples/tanstack-ssr/src/server.ts
index 98c671d1..276e8763 100644
--- a/examples/tanstack-ssr/src/server.ts
+++ b/examples/tanstack-ssr/src/server.ts
@@ -40,7 +40,9 @@ const server = {
cookies,
cookieOptions,
});
- const token = await session.getToken();
+ // The build's prerender renders without a visitor, so its page, which every visitor gets, must
+ // not say signed out: vite.config.ts marks its requests, and they get no token at all.
+ const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
const headers = new Headers(page.headers);
cookies.applyTo(headers);
diff --git a/examples/tanstack-ssr/vite.config.ts b/examples/tanstack-ssr/vite.config.ts
index c1fa9a2d..8e68d2ff 100644
--- a/examples/tanstack-ssr/vite.config.ts
+++ b/examples/tanstack-ssr/vite.config.ts
@@ -8,8 +8,12 @@ export default defineConfig({
plugins: [
convexSsr(),
// Prerenders /about only. `prerender: { enabled: true }` would also prerender every static
- // route, and crawling would follow /about's links.
- tanstackStart({ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }] }),
+ // route, and crawling would follow /about's links. The header marks the prerender's requests
+ // for src/server.ts; a prerender renders without a visitor.
+ tanstackStart({
+ prerender: { headers: { "x-prerender": "1" } },
+ pages: [{ path: "/about", prerender: { enabled: true, crawlLinks: false } }],
+ }),
react(),
],
});
diff --git a/packages/convex-hosting-tanstack-start/CHANGELOG.md b/packages/convex-hosting-tanstack-start/CHANGELOG.md
index 0cd97525..3b03fa9c 100644
--- a/packages/convex-hosting-tanstack-start/CHANGELOG.md
+++ b/packages/convex-hosting-tanstack-start/CHANGELOG.md
@@ -8,6 +8,11 @@
override of 0.6.0 is gone with it. `convexSsr()` resolves `node:stream` to a stub for the server
build: TanStack imports Node's stream classes for a branch that never runs where React's Web
Stream renderer exists, and Convex's bundler had nothing to resolve them to.
+- The request context's token may be `undefined`: a render that knows nothing about a session, the
+ build's prerender, whose page every visitor gets. `SignedIn` and `SignedOut` wait for the browser
+ on it, and `redirectWithoutSession` redirects it like a render without a session. The app's
+ `src/server.ts` passes `undefined` for the prerender's requests, which it tells apart by the
+ request header that `tanstackStart({ prerender: { headers } })` sends.
## 0.8.0
diff --git a/packages/convex-hosting-tanstack-start/README.md b/packages/convex-hosting-tanstack-start/README.md
index 0cd64f2c..09c47caa 100644
--- a/packages/convex-hosting-tanstack-start/README.md
+++ b/packages/convex-hosting-tanstack-start/README.md
@@ -107,17 +107,24 @@ does in its
[`src/server.ts`](https://github.com/samebase/convex-hosting/blob/main/examples/tanstack-ssr/src/server.ts):
```ts
-const token = await session.getToken();
+const token = request.headers.has("x-prerender") ? undefined : await session.getToken();
const page = await handler.fetch(request, { context: { token } });
```
+The build's prerender renders a page without a visitor, and every visitor gets that page, so its
+render must not say signed out. TanStack's prerender sends the request headers of
+`tanstackStart({ prerender: { headers } })`; the example sets one, and `src/server.ts` gives such a
+request no token at all. `SignedIn` and `SignedOut` then wait for the browser on that page, and a
+gate redirects it as it redirects a request without a session.
+
`convex/http.ts` then forwards every request that is not a GET, and the page render, to the build
with `server.fetch(request)`, as the example's
[`convex/http.ts`](https://github.com/samebase/convex-hosting/blob/main/examples/tanstack-ssr/convex/http.ts)
does. `@samebase/convex-hosting-tanstack-start/convex-auth` has the rest, and loading it declares
-`requestContext` as `{ token: string | null }` in the `Register` of `@tanstack/react-start`, which
-makes `context` required and typed in `handler.fetch` and gives every route a typed `serverContext`.
-The app creates its Convex client with `expectAuth: true`.
+`requestContext` as `{ token: string | null | undefined }` in the `Register` of
+`@tanstack/react-start`, which makes `context` required and typed in `handler.fetch` and gives every
+route a typed `serverContext`. The token is `undefined` for a render that knows nothing about a
+session, the prerender. The app creates its Convex client with `expectAuth: true`.
Nothing in this entry is about hosting: it is the TanStack Start integration that Convex Auth does
not ship, and the
diff --git a/packages/convex-hosting-tanstack-start/src/convex-auth.tsx b/packages/convex-hosting-tanstack-start/src/convex-auth.tsx
index 25f39b8b..7ce032a4 100644
--- a/packages/convex-hosting-tanstack-start/src/convex-auth.tsx
+++ b/packages/convex-hosting-tanstack-start/src/convex-auth.tsx
@@ -32,11 +32,13 @@ import { useConvexAuth, type ConvexReactClient } from "convex/react";
import { createContext, useContext, useEffect, useRef, type ReactNode } from "react";
/**
- * The request context of a page render: the session's access token, or `null` without one. The
- * app's `src/server.ts` reads it off the cookies and passes it to the server entry.
+ * The request context of a page render: the session's access token, `null` without one, or
+ * `undefined` for a render that knows nothing about a session: the build's prerender, whose page
+ * every visitor gets. The app's `src/server.ts` reads it off the cookies and passes it to the
+ * server entry.
*/
interface RequestContext {
- token: string | null;
+ token: string | null | undefined;
}
// Start's own `Register`, where it reads `requestContext`. TypeScript 5.9 does not merge a
@@ -104,8 +106,8 @@ function ReleaseWhenSignedOut({ client }: { client: ConvexReactClient }) {
*
* Convex Auth reports loading during the server render and the hydration, so the token of the
* server render decides until the provider settles: a string means that the server rendered as the
- * user, and `null` that it found no session; a render without a request context has none. Both
- * renders read the same token, so the hydration matches.
+ * user, and `null` that it found no session; `undefined`, the build's prerender or a render without
+ * a request context, knows nothing. Both renders read the same token, so the hydration matches.
*/
function useSignedIn(): boolean | undefined {
const token = useContext(SessionToken);
@@ -144,14 +146,15 @@ export function useSignOutSession(url: string): () => Promise {
}
/**
- * A gate's `beforeLoad`: a server render without a session throws `redirect(options)`. Only the
- * server render has the cookie; a browser navigation has no `serverContext`.
+ * A gate's `beforeLoad`: a server render without a session throws `redirect(options)`, whether it
+ * found none or knows nothing about one. Only the server render has the cookie; a browser
+ * navigation has no `serverContext`.
*/
export function redirectWithoutSession(
serverContext: RequestContext | undefined,
options: ValidateRedirectOptions,
) {
- if (serverContext?.token === null) {
+ if (serverContext !== undefined && !serverContext.token) {
throw redirect<
RegisteredRouter,
InferTo,