Skip to content

Remix 3 SSR: server-rendered, hydrated SPA for all HTML pages - #601

Merged
kentcdodds merged 37 commits into
mainfrom
cursor/remix-ssr-hydration-c537
Jul 4, 2026
Merged

kentcdodds merged 37 commits into
mainfrom
cursor/remix-ssr-hydration-c537

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 4, 2026 •

Copy link
Copy Markdown
Member

Remix 3 SSR: server-rendered, hydrated SPA

Converts the browser app from a client-only SPA (spinner shell + client fetch) to a server-rendered, then hydrated SPA. Every HTML route now streams fully rendered HTML (real page content, session-aware nav, embedded route data) via renderToStream from remix/ui/server, and the client hydrates in place via run() + a single clientEntry app root — after which the existing client router keeps SPA navigation.

ssr_hydrated_spa_full_demo.mp4

What changed

Core SSR pipeline

  • ssr-render.tsx / ssr-document.tsx: renderAppPage() loads the session server-side and streams a full HTML document with the hydrated AppRoot clientEntry (/client-entry.js#AppRoot).
  • client/entry.tsx now boots with run({ loadModule }) (registry pattern for the single esbuild bundle) instead of createRoot().render() + the old replaceChildren() anti-hydration hack.
  • router-location.tsx: current URL is injected via context (SSR) or window.location + navigation events (client) — no window.location reads in render paths.
  • Session is embedded in the hydration payload, so the nav renders correctly on first paint with no /session fetch flash. Client-side refresh still happens on SPA navigations.

Loader-data convention (no double fetch)

  • Each route's data logic is extracted into a pure *-data.ts loader shared by the JSON endpoint and the HTML handler — zero duplication.
  • HTML handlers await the loader and embed the payload; route components consume embedded data only when the current URL matches the SSR URL (one-time consumption via tryConsumeEmbeddedLoaderData), falling back to the existing fetch on SPA navigation.
  • Converted routes: home, privacy, community index/detail, login/signup, reset-password (new HTML route — password-reset email links previously 404'd), oauth callback, oauth authorize, account (profile, integrations, remote connectors, package invocation tokens incl. /:tokenId, secrets incl. subpaths), admin (users, roles, community reports). /connect/oauth intentionally keeps its client-only multi-step flow behind an SSR-safe shell.

Performance & caching

  • HTML responses: Cache-Control: no-store (every page embeds session — must never be shared-cached).
  • Public community data: in-isolate TTL cache (60s, bounded at 256 entries) with version-stamp invalidation on every listing mutation; viewer-specific fields (loggedIn, forkPrompt) composed per request, never cached. Observability via X-Kody-Cache: hit|miss.
  • Per-request auth dedup: session + roles/permissions loaded once per request across SSR and handlers.
  • /client-entry.js and /styles.css served with public, max-age=31536000, immutable keyed by a ?v=<commitSha> build id, plus <link rel="modulepreload"> so hydration JS fetches during HTML streaming.
  • User-scoped data is deliberately NOT cached across requests; per-user isolation is preserved (loaders take the authenticated user).

Tests

  • New ssr-render.node.test.ts: SSR HTML contains real content + embedded payloads; auth redirects; 404 SSR; reset-password modes.
  • New e2e/ssr-hydration.spec.ts: raw-response SSR assertions, SPA navigation without full reload (window-marker), hydrated search interactivity, 404 SPA recovery.
  • Extended smoke.spec.ts and admin-rbac.spec.ts with SPA-navigation regressions (same-origin refetch, admin list rendering).
  • data-cache.node.test.ts, loader-data-context.node.test.ts, auth-redirect.node.test.ts, auth-page.node.test.ts, account.node.test.ts, oauth-handlers.workers.test.ts for the new utilities and auth flows.

Screenshots

Community page server-rendered (logged out)
Admin users after SPA navigation

Raw SSR HTML evidence (curl): community_ssr_raw_html_excerpt.log

Validation

  • npm run validate fully green: format, lint, typecheck, unit tests, Playwright E2E specs, MCP E2E.
  • Manual browser demo (video above): SSR first paint, hydrated search, SPA navigation, login, authed account/secrets/admin pages.

Review follow-ups (Bugbot + CodeRabbit)

Four review rounds addressed; the latest Bugbot run passes with zero findings. Notable fixes on top of the original change:

  • SSR 404 no longer blocks later SPA navigation; session silently revalidates after hydration.
  • Embedded loader data is consumed one-time per key only when the URL matches the SSR URL (guard-before-consume everywhere, including community detail id checks).
  • Routes transition to a loading state whenever the current URL diverges from loaded data — no stale lists or wrong package during SPA transitions.
  • /oauth/authorize now embeds authorize info (client + scopes, or error state) in the SSR payload instead of streaming a loading shell.
  • Stale session cookies: redirect-to-login responses now carry the session-destroy Set-Cookie, and the login page validates the user in D1 — fixes a redirect loop.
  • Token selection URL updates sync lastLoadedHref before navigate() to avoid redundant refetches.
  • Data cache is bounded (256 entries, lazy expiry sweep); styles.css versioned + immutable like the client bundle.
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ ed933a5 · Head: 52530dd

Classification: extends — the browser app surface's rendering contract changes from client-rendered shell to streamed SSR + hydration; sessions and community listings gain shared loaders/caching but keep their contracts.

Primitives touched

Primitive Group Impact
app-ui surfaces extends — SSR document pipeline, loader-data convention, new /reset-password route, hydration boot
app-sessions auth composes — session read shared with SSR via session-info.ts, per-request dedup
mcp-oauth auth composes — OAuth pages render through the SSR document
community-listings assistant extends — public payload TTL cache with mutation invalidation
rbac auth composes — admin HTML embeds role-gated data behind existing guards

System map

flowchart LR
	appUi["app-ui"]:::extended
	appSessions["app-sessions"]:::touched
	mcpOauth["mcp-oauth"]:::touched
	rbac["rbac"]:::touched
	communityListings["community-listings"]:::extended
	d1AppDb["d1-app-db"]:::untouched
	appUi --> appSessions --> d1AppDb
	appUi --> communityListings --> d1AppDb
	appUi --> rbac
	mcpOauth --> appUi
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Before / after

Concern Before After
HTML response Static spinner shell Streamed SSR with content + embedded session/loader data
Client boot createRoot().render() from scratch run() hydration of server DOM
Page data Client fetch after mount Embedded at SSR; fetch only on SPA navigation
/reset-password GET 404 SSR page (request + confirm modes)
/client-entry.js No cache strategy ?v=<commitSha> + immutable, modulepreload

Invariants

  • per-user-isolation: preserved — loaders take the authenticated user; only public community payloads are cached (viewer-specific fields composed per request); all HTML is no-store.
  • community-listing-isolation: preserved — public read stays limited to active listings; cache invalidated on publish/unpublish/fork/rate/moderation.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features
    • Improved SSR-to-SPA hydration across Community, Account, and Admin with embedded-data reuse to reduce unnecessary reloads.
    • Enhanced SPA navigation and query preservation (including Secrets and OAuth-related SPA shells).
    • Added richer server-rendered page metadata and standardized “Not found” responses.
  • Bug Fixes
    • Fixed client navigation/refetch issues to keep routing on the correct in-app path and origin.
    • Improved Community public cache invalidation so updates surface promptly.
  • Tests
    • Added/expanded end-to-end and SSR/hydration regression coverage for admin navigation, Community flows, SPA search, and Secrets navigation.

… run() hydration, injected router location, session + loader-data embedding for home/privacy/community
…d embedded loader data; add /reset-password HTML route
…ned immutable assets, modulepreload), SSR test coverage, and URL-scoped one-time embedded loader data consumption
…roke SPA refetch) and cover secrets SPA navigation in smoke E2E
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds a shared SSR rendering path, typed loader-data contracts, request-scoped auth/session resolution, and router-aware client hydration. Worker handlers and client routes now use shared loaders and embedded SSR data instead of direct window reads.

Changes

SSR, loaders, and router-aware app runtime

Layer / File(s) Summary
Client entry and router contracts
packages/worker/client/app-root.tsx, packages/worker/client/entry.tsx, packages/worker/client/client-router.tsx, packages/worker/client/router-location.tsx, packages/worker/client/loader-data-context.tsx, packages/worker/client/loader-data-types.ts, packages/worker/client/app.tsx
Adds the client root entry, module-based client startup, router location/context helpers, embedded loader-data context, loader-data types, and app-side session/router initialization.
SSR document, auth, and response rendering
packages/worker/src/app/client-build-id.ts, packages/worker/src/app/ssr-stubs/app-root.ts, packages/worker/src/app/ssr-document.tsx, packages/worker/src/app/ssr-render.tsx, packages/worker/src/app/session-info.ts, packages/worker/src/app/request-auth-cache.ts, packages/worker/src/app/request-cache.ts, packages/worker/src/app/authenticated-user.ts
Adds SSR document rendering, session loading, auth caching, request cache metadata, and SSR test stubs.
Loader data, caching, and feature payloads
packages/worker/src/app/loader-data.ts, packages/worker/src/app/community-api.ts, packages/worker/src/app/community-data.ts, packages/worker/src/app/data-cache.ts, packages/worker/src/app/admin-*-data.ts, packages/worker/src/app/account-*-data.ts, packages/worker/src/community/service.ts
Adds shared loader contracts, cache-backed community loaders, per-feature account/admin loaders, and cache invalidation on community mutations.
Worker handlers and router wiring
packages/worker/src/app/handlers/*.ts(x), packages/worker/src/app/router.ts, packages/worker/src/app/routes.ts, packages/worker/src/index.ts, packages/worker/src/oauth-handlers.ts
Rewires worker handlers to shared loaders and renderAppPage, updates router/fallback/OAuth handling, and adjusts static asset caching.
Client routes using router state and embedded loader data
packages/worker/client/routes/*.tsx
Updates account, admin, community, login, OAuth, and reset-password routes to use router-derived URLs and embedded SSR data.
TypeScript and worker config
packages/worker/project.json, packages/worker/tsconfig*.json, packages/worker/wrangler.jsonc, tsconfig.json
Splits worker typechecking config, adds JSX/client-entry support, and marks client-entry as worker-first.
E2E coverage
e2e/admin-rbac.spec.ts, e2e/d1-utils.ts, e2e/smoke.spec.ts, e2e/ssr-hydration.spec.ts
Adds admin navigation assertions, a community listing seed helper, a secrets SPA navigation check, and SSR hydration coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • kentcdodds/kody#61: Both PRs touch the account secrets handler/client flow and its shared data plumbing.
  • kentcdodds/kody#393: Both PRs modify the remote connectors account UI and its supporting route logic.
  • kentcdodds/kody#599: Both PRs extend the community page SSR/SPA behavior and the related E2E coverage.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving HTML pages to SSR with hydration for the SPA.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/remix-ssr-hydration-c537

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kentcdodds
kentcdodds marked this pull request as ready for review July 4, 2026 09:23
Comment thread packages/worker/client/app.tsx Resolved
@github-actions

github-actions Bot commented Jul 4, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-601.kentcdodds.workers.dev

Worker: kody-pr-601
D1: kody-pr-601-db
KV: kody-pr-601-oauth-kv

Mocks:

…retry storm exhausted auth rate limit in CI)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/worker/client/routes/community-detail.tsx (1)

96-139: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Infinite retry loop for a 404/errored community listing.

lastLoadedListingId is only updated on the success path (Line 128). The 404 branch (Lines 111-116) and catch branch (Lines 130-138) leave it unset, so the render check at Line 230-233 (listingId && listingId !== lastLoadedListingId) stays true forever, repeatedly re-queuing loadCommunityDetail for a listing that will keep 404ing.

🐛 Proposed fix: stamp lastLoadedListingId on all terminal outcomes
 	async function loadCommunityDetail() {
 		const listingId = getCurrentListingId(handle)
 		if (!listingId) {
 			status = 'error'
 			message = 'Community package not found.'
 			handle.update()
 			return
 		}

 		const requestId = ++loadRequestId
 		try {
 			const response = await fetch(buildDetailApiPath(listingId), {
 				headers: { Accept: 'application/json' },
 			})
 			if (requestId !== loadRequestId) return
 			if (response.status === 404) {
 				status = 'error'
 				message = 'Community package not found.'
+				lastLoadedListingId = listingId
 				handle.update()
 				return
 			}
 			...
 		} catch (error) {
 			if (requestId !== loadRequestId) return
 			status = 'error'
 			message =
 				error instanceof Error
 					? error.message
 					: 'Unable to load community package.'
+			lastLoadedListingId = listingId
 			handle.update()
 		}
 	}

Also applies to: 227-240

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/community-detail.tsx` around lines 96 - 139,
The community detail loader can retry forever because loadCommunityDetail only
updates lastLoadedListingId on success, leaving 404 and catch paths to
re-trigger the render guard in the community-detail route. Update
loadCommunityDetail so terminal failures also stamp lastLoadedListingId (or
otherwise mark the listing as handled) in the 404 branch and the catch block,
keeping the retry check from re-queuing the same listing. Use the existing
identifiers loadCommunityDetail, lastLoadedListingId, and the render condition
around listingId !== lastLoadedListingId to locate the fix.
packages/worker/client/routes/admin-roles.tsx (1)

33-67: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Infinite retry loop when the admin-roles fetch fails.

lastLoadedHref is only assigned on the success path (Line 58). The 403 branch (Lines 45-50) and the catch branch (Lines 60-66) leave it at ''. Since the render check at Line 92 (status === 'loading' || currentHref !== lastLoadedHref) stays true forever after an error, every handle.update() re-triggers applyEmbeddedLoaderData → handle.queueTask(loadAdminRoles) again, forming an unbounded fetch loop against /admin/roles.json whenever the user lacks permission or the network fails.

🐛 Proposed fix: stamp lastLoadedHref before starting the request
 	async function loadAdminRoles() {
 		const requestId = ++loadRequestId
+		const href = readCurrentRouterHref(handle)
 		try {
 			const response = await fetch(adminRolesApiPath, {
 				headers: { Accept: 'application/json' },
 				credentials: 'include',
 			})
 			if (requestId !== loadRequestId) return
 			if (response.status === 401) {
 				window.location.assign('/login')
 				return
 			}
 			if (response.status === 403) {
 				status = 'error'
 				message = 'You do not have permission to view admin roles.'
+				lastLoadedHref = href
 				handle.update()
 				return
 			}
 			const payload = await readJson<AdminRolesLoaderData>(response)
 			if (!response.ok || !payload?.ok) {
 				throw new Error('Unable to load admin roles.')
 			}
 			roles = payload.roles
 			status = 'ready'
 			message = null
-			lastLoadedHref = readCurrentRouterHref(handle)
+			lastLoadedHref = href
 			handle.update()
 		} catch (error) {
 			if (requestId !== loadRequestId) return
 			status = 'error'
 			message =
 				error instanceof Error ? error.message : 'Unable to load admin roles.'
+			lastLoadedHref = href
 			handle.update()
 		}
 	}

Also applies to: 89-99

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-roles.tsx` around lines 33 - 67, The
admin roles loader can retrigger forever after a failed fetch because
lastLoadedHref is only set on the success path in loadAdminRoles. Update
loadAdminRoles in admin-roles.tsx so lastLoadedHref is stamped before issuing
the request (or otherwise set on every attempt), ensuring the render guard
comparing currentHref to lastLoadedHref does not keep scheduling
applyEmbeddedLoaderData after 401/403/error paths. Keep the existing
status/message handling intact, and make sure the load request flow around
loadRequestId and handle.update() still works.
packages/worker/client/routes/admin-users.tsx (1)

75-123: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Same infinite retry loop bug as admin-roles.tsx on load failure.

lastLoadedHref is set only on the success path (Line 103). The 403 branch (Lines 88-93) and catch branch (Lines 116-122) never set it, so the render check at Line 223 (status === 'loading' || currentHref !== lastLoadedHref) remains true after every failed load, causing loadAdminUsers to be re-queued and re-fail indefinitely.

🐛 Proposed fix: stamp lastLoadedHref on all outcomes
 	async function loadAdminUsers() {
 		const href = readCurrentRouterHref(handle)
 		const requestId = ++loadRequestId
 		try {
 			const response = await fetch(
 				`${adminUsersApiPath}${readRouterSearch(handle)}`,
 				{ headers: { Accept: 'application/json' }, credentials: 'include' },
 			)
 			if (requestId !== loadRequestId) return
 			if (response.status === 401) {
 				window.location.assign('/login')
 				return
 			}
 			if (response.status === 403) {
 				status = 'error'
 				message = 'You do not have permission to view admin users.'
+				lastLoadedHref = href
 				handle.update()
 				return
 			}
 			...
 		} catch (error) {
 			if (requestId !== loadRequestId) return
 			status = 'error'
 			message =
 				error instanceof Error ? error.message : 'Unable to load admin users.'
+			lastLoadedHref = href
 			handle.update()
 		}
 	}

Also applies to: 217-230

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-users.tsx` around lines 75 - 123,
`loadAdminUsers` can get stuck in an infinite retry loop because
`lastLoadedHref` is only updated on the success path, so failed loads never
satisfy the render guard that compares `currentHref` to `lastLoadedHref`. Update
`lastLoadedHref` for every terminal outcome in `loadAdminUsers`—including the
403 branch and the catch path—so the loader doesn’t re-queue after a failure.
Keep the existing success behavior, and make sure the render condition that
checks `status === 'loading' || currentHref !== lastLoadedHref` can settle after
any completed request.
🧹 Nitpick comments (12)
e2e/ssr-hydration.spec.ts (1)

56-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Static analysis flags dynamic RegExp construction.

alphaListing.listingId is a fixed test constant, not attacker-controlled input, so the ReDoS/injection risk here is negligible in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/ssr-hydration.spec.ts` around lines 56 - 59, The URL assertion in
ssr-hydration.spec.ts uses dynamic RegExp construction with
alphaListing.listingId, which triggers static analysis even though the value is
a fixed test constant. Update the page.toHaveURL check in the relevant test to
avoid constructing the pattern dynamically, and use a safer direct assertion or
a static regex shape in the assert around alphaListing and its listingId.

Source: Linters/SAST tools

packages/worker/client/loader-data-types.ts (1)

77-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated AccountIntegrationListItem/flow type risks drifting from the shared contract.

This file's AccountIntegrationListItem.flow is an inline 'pkce' | 'confidential' union, while routes/account-integrations.tsx independently defines a same-named type using flow: IntegrationFlow. The file's own doc comment states these loader types are meant to be shared with route components and JSON API shapes — having two separately maintained definitions undermines that.

Consider importing and reusing the shared IntegrationFlow type here instead of re-declaring the union.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/loader-data-types.ts` around lines 77 - 96,
`AccountIntegrationListItem.flow` is duplicating the shared `IntegrationFlow`
contract, which can drift from the route/component type definitions. Update
`AccountIntegrationListItem` in `loader-data-types.ts` to reuse the existing
`IntegrationFlow` type instead of the inline union, keeping the loader shape
aligned with `routes/account-integrations.tsx` and the shared API contract.
packages/worker/src/app/handlers/auth-page.ts (1)

11-36: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Login and signup pages will share the same (generic) <title>.

createAuthPageHandler now backs both /login and /signup (via createLoginHandler/createSignupHandler), but renderAppPage({ request, env }) at Line 36 passes no title, so both routes fall back to whatever generic default SsrDocument uses. Now that login/signup are separate factory calls, it's cheap to pass a distinguishing title through.

♻️ Proposed fix to differentiate titles
-export function createAuthPageHandler(env: Env) {
+export function createAuthPageHandler(env: Env, title: string) {
 	return {
 		middleware: [],
 		async handler({ request }: { request: Request }) {
 			...
-			return renderAppPage({ request, env })
+			return renderAppPage({ request, env, title })
 		},
 	}
 }
-export function createLoginHandler(env: Env) {
-	return createAuthPageHandler(env) satisfies Action<typeof routes.login>
+export function createLoginHandler(env: Env) {
+	return createAuthPageHandler(env, 'Log in') satisfies Action<typeof routes.login>
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/auth-page.ts` around lines 11 - 36,
createAuthPageHandler currently calls renderAppPage({ request, env }) without a
page title, so both createLoginHandler and createSignupHandler end up using the
same default SsrDocument title. Update the auth-page flow to pass a
distinguishing title through renderAppPage for each route, using the
createAuthPageHandler / createLoginHandler / createSignupHandler entry points to
select the appropriate login or signup title while keeping the existing redirect
and session logic unchanged.
packages/worker/src/app/handlers/admin-roles.ts (1)

16-27: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Remove the redundant session check. requireUserWithRole(request, env, 'admin') already rejects unauthenticated requests through the same auth path, so the follow-up readAuthSessionResult(request)/redirectToLogin(request) branch can be dropped.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/admin-roles.ts` around lines 16 - 27, The
admin role handler currently performs a redundant authentication fallback after
requireUserWithRole(request, env, 'admin'). Remove the extra
readAuthSessionResult(request) and redirectToLogin(request) branch from the
admin-roles handler, since requireUserWithRole already handles unauthenticated
requests; keep the existing Response passthrough and error rethrow behavior
intact.
packages/worker/client/routes/login.tsx (1)

47-64: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: inconsistent use of the safe getPathname wrapper vs raw readRouterPathname.

getPathname(handle) (defined in client-router.tsx) wraps readRouterPathname in a try/catch with a window fallback, but getCurrentAuthMode/getCurrentRedirectTo call readRouterPathname/readRouterSearch directly. Since LoginRoute only runs inside the router-provided tree this is likely safe in practice, but the asymmetry is worth a second look in case these helpers are ever reused outside that context.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/login.tsx` around lines 47 - 64, The auth
helper logic in LoginRoute is bypassing the safer router wrappers by calling
readRouterPathname and readRouterSearch directly in getCurrentAuthMode and
getCurrentRedirectTo. Update these helpers to use the existing getPathname-style
fallback approach (or equivalent safe wrappers from client-router.tsx) so
pathname/search access stays consistent and resilient if reused outside the
router tree.
packages/worker/client/routes/admin-roles.tsx (1)

20-24: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the is*Path helper pattern.

isAdminRolesPath mirrors near-identical helpers in admin-users.tsx, account-integrations.tsx, account-remote-connectors.tsx, etc. A small shared pathnameOf(href)/matchesPath utility would reduce duplication across route files.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-roles.tsx` around lines 20 - 24, The
isAdminRolesPath helper duplicates the same pathname parsing logic found in
other route files, so extract the shared pattern into a common utility such as
pathnameOf(href) and/or matchesPath, then update this file and the similar
helpers in admin-users.tsx and
account-integrations.tsx/account-remote-connectors.tsx to use it. Keep the
route-specific path comparison in each caller, but centralize the
URL-to-pathname normalization to remove repeated new URL(href,
'http://localhost').pathname logic.
packages/worker/client/routes/account-integrations.tsx (1)

150-183: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Duplicated router-hydration boilerplate across seven route files.

This exact listenToRouterNavigation + applyEmbeddedLoaderData + queueTask fallback block is copy-pasted nearly verbatim into account.tsx, account-package-invocation-tokens.tsx, account-remote-connectors.tsx, account-secrets.tsx, community.tsx, and admin-community-reports.tsx. The duplication has already caused drift: admin-community-reports.tsx's listener omits the status !== 'loading' guard that every other copy has (harmless today, but shows how easy it is for these copies to diverge).

Consider extracting a small shared helper (e.g. in client-router.tsx or loader-data-context.tsx) that encapsulates: register nav listener → transition to loading → try embedded data → fallback to queueTask. This would remove ~30 duplicated lines per route and prevent this kind of silent divergence going forward.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/account-integrations.tsx` around lines 150 -
183, The router-hydration logic is duplicated across multiple route components
and has already drifted, so extract the shared navigation/loading/embedded-data
fallback flow into a reusable helper. Move the repeated
`listenToRouterNavigation`, `applyEmbeddedLoaderData`, and
`queueTask(loadIntegrations)` pattern into a common utility such as
`client-router.tsx` or `loader-data-context.tsx`, then update `account.tsx`,
`account-package-invocation-tokens.tsx`, `account-remote-connectors.tsx`,
`account-secrets.tsx`, `community.tsx`, and `admin-community-reports.tsx` to
call it. Keep the existing `handle`, `readCurrentRouterHref`,
`tryConsumeEmbeddedLoaderData`, and `loadIntegrations` behavior intact while
centralizing the `status` transition and embedded-data check.
packages/worker/src/app/request-auth-cache.ts (1)

80-89: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider surfacing role-lookup failures beyond console.error.

On getUserRolesAndPermissions failure, the user is still authenticated but silently downgraded to zero roles/permissions. This fail-closed behavior is reasonable, but a transient DB blip could cause admins to see authz-denied pages that look like a permissions bug rather than an outage. A metric/counter here would make this failure mode observable in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/request-auth-cache.ts` around lines 80 - 89, The
role/permission lookup in request-auth-cache.ts only logs to console when
getUserRolesAndPermissions fails, so add an observable signal there such as a
metric/counter or structured app logger entry in the catch block around the
getUserRolesAndPermissions call. Use the existing request auth path in
request-auth-cache.ts and the getUserRolesAndPermissions flow to record lookup
failures while preserving the current fail-closed empty roles/permissions
fallback.
packages/worker/src/app/account-secrets-data.ts (2)

320-431: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Loader-data shapes duplicated locally instead of reused from loader-data.ts.

AccountSecretListItem/AccountSecretDetail (and the analogous SecretApprovalView) are redefined here even though equivalent shapes are already exported from #app/loader-data.ts. Same pattern shows up in admin-community-reports-data.ts. Importing the shared types would avoid drift if one side is updated without the other. Also, the two trailing export {} statements (lines 428-431) could be merged into one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/account-secrets-data.ts` around lines 320 - 431, The
loader-data types are duplicated in this module instead of reusing the shared
exports from loader-data.ts, which can drift over time. Update
account-secrets-data.ts to import and use the existing shared shapes for
AccountSecretListItem, AccountSecretDetail, and the analogous SecretApprovalView
where applicable, and align any matching code in toAccountSecretListItem and
related helpers to those shared types. Also consolidate the trailing export
statements at the end of the file into a single export block.

77-88: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Dead validation branch in readSelectedSecretIdFromPath.

parsedLegacyId is computed but doesn't affect the return value: both branches return legacySecretId whenever it's non-empty, and null only when it's empty — identical to return legacySecretId || null. This reads as if invalid legacy ids are being rejected, but they aren't.

♻️ Proposed simplification (or fix if rejection was intended)
 	if (pathname.startsWith(`${secretsBasePath}/`)) {
 		const legacySecretId = pathname.slice(`${secretsBasePath}/`.length)
-		const parsedLegacyId = parseAccountSecretId(legacySecretId)
-		return parsedLegacyId ? legacySecretId : legacySecretId || null
+		return legacySecretId || null
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/account-secrets-data.ts` around lines 77 - 88,
`readSelectedSecretIdFromPath` contains a dead validation branch because
`parsedLegacyId` is computed but never changes the returned value. Either remove
the unused `parseAccountSecretId` check and simplify the legacy path handling to
return the sliced legacy id or null, or, if invalid legacy ids should be
rejected, change the `parseAccountSecretId` result to actually gate the return
in the `readSelectedSecretIdFromPath` function.
packages/worker/src/app/request-cache.ts (1)

18-29: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Single-slot cache-lookup store loses information if a route composes multiple cached loaders.

setRequestDataCacheLookup overwrites any prior value, so if a future handler calls multiple cache-backed loaders for one request, only the last hit/miss status survives into the X-Kody-Cache header. Not an issue today (each current handler calls it at most once), but worth keeping in mind before adding multi-loader pages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/request-cache.ts` around lines 18 - 29, The request
cache metadata currently stores only one DataCacheLookup via
setRequestDataCacheLookup, so later loaders can overwrite earlier cache
hits/misses before getRequestDataCacheLookup reads them back for the
X-Kody-Cache header. Update the requestMetaStore-backed request metadata to
accumulate or merge multiple lookups per request instead of replacing the
previous value, and adjust getRequestDataCacheLookup and any header-building
code to consume the combined result; use setRequestDataCacheLookup and
getRequestDataCacheLookup as the key entry points.
packages/worker/src/app/data-cache.ts (1)

65-78: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No de-duplication of concurrent cache misses (thundering herd).

getOrSetDataCache re-invokes load() for every concurrent request that misses the same key, rather than sharing one in-flight promise. Under a burst of requests right after TTL expiry (e.g. a popular community listing), this can cause N duplicate D1 reads instead of one.

♻️ Suggested in-flight promise de-duplication
+const inFlight = new Map<string, Promise<unknown>>()
+
 export async function getOrSetDataCache<T>(input: {
 	key: string
 	ttlMs?: number
 	load: () => Promise<T>
 }): Promise<{ value: T; lookup: DataCacheLookup }> {
 	const cached = peekDataCache<T>(input.key)
 	if (cached !== undefined) {
 		return { value: cached, lookup: 'hit' }
 	}
 
-	const value = await input.load()
-	setDataCache(input.key, value, input.ttlMs)
-	return { value, lookup: 'miss' }
+	let pending = inFlight.get(input.key) as Promise<T> | undefined
+	if (!pending) {
+		pending = input.load().finally(() => inFlight.delete(input.key))
+		inFlight.set(input.key, pending)
+	}
+	const value = await pending
+	setDataCache(input.key, value, input.ttlMs)
+	return { value, lookup: 'miss' }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/data-cache.ts` around lines 65 - 78,
getOrSetDataCache currently only caches completed values, so concurrent misses
for the same key all call load() independently. Update getOrSetDataCache to
de-duplicate in-flight loads by tracking a per-key pending promise in the
data-cache module and having later callers await it instead of starting a new
load. Keep the existing cache hit/miss behavior intact and clear the in-flight
entry after the promise settles; use getOrSetDataCache, peekDataCache, and
setDataCache as the main touchpoints.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@e2e/d1-utils.ts`:
- Around line 86-135: The upsert in seedCommunityListingInE2eDatabase leaves
owner_user_id stale on reseed because it is not included in the ON CONFLICT DO
UPDATE SET clause. Update the conflict handling so the reseeded record refreshes
owner_user_id alongside the other mutable fields, using the same ownerUserId
derived from input.ownerEmail. Keep the fix within
seedCommunityListingInE2eDatabase and preserve the existing insert/default
behavior for listingId, packageId, sourceId, and kodyId.

In `@packages/worker/client/client-router.tsx`:
- Around line 308-329: The Router render logic currently relies on
handle.props.notFound, which is only set from the initial SSR payload and can
stay stuck on client-side navigation. Update Router and/or
listenToRouterNavigation so the notFound state is cleared or tracked per
pathname change before rendering, and use readRouterPathname plus matchRoute to
decide when to stop showing handle.props.fallback. Keep the fix localized to
Router, handle.update, and the navigation listener so SPA transitions can
recover from an initial 404.

In `@packages/worker/src/app/admin-community-reports-data.ts`:
- Around line 18-47: `loadAdminCommunityReportsData` is returning the raw query
value for `statusFilter`, but `loadAdminCommunityReports` applies normalization
before fetching reports, so the response can disagree with the actual filter
used. Normalize the status once in `loadAdminCommunityReportsData` (using
`readReportStatusFilter` or the same `all`/default handling as
`loadAdminCommunityReports`) and pass that normalized value into
`loadAdminCommunityReports`, then return that same value in the `statusFilter`
field so the loader data matches the displayed reports.

In `@packages/worker/src/app/data-cache.ts`:
- Line 20: The in-memory cache in data-cache.ts is unbounded because `store`
only grows via `setDataCache` and expired entries are removed lazily in
`peekDataCache`, so unique `q` values can accumulate indefinitely. Update the
cache behavior around `store`, `setDataCache`, and `peekDataCache` to enforce a
maximum size with oldest/LRU eviction or add a periodic expired-entry sweep so
entries do not remain past TTL unless they are revisited.

In `@packages/worker/src/app/ssr-document.tsx`:
- Around line 46-47: The SSR document still hardcodes the stylesheet as
/styles.css while clientEntryHref is versioned, so update the ssr-document
rendering to version the stylesheet link too. In ssr-document.tsx, keep the
existing modulepreload behavior but change the stylesheet reference to use the
same build-id/query-param approach as clientEntryHref so deploys don’t serve
stale CSS.

---

Outside diff comments:
In `@packages/worker/client/routes/admin-roles.tsx`:
- Around line 33-67: The admin roles loader can retrigger forever after a failed
fetch because lastLoadedHref is only set on the success path in loadAdminRoles.
Update loadAdminRoles in admin-roles.tsx so lastLoadedHref is stamped before
issuing the request (or otherwise set on every attempt), ensuring the render
guard comparing currentHref to lastLoadedHref does not keep scheduling
applyEmbeddedLoaderData after 401/403/error paths. Keep the existing
status/message handling intact, and make sure the load request flow around
loadRequestId and handle.update() still works.

In `@packages/worker/client/routes/admin-users.tsx`:
- Around line 75-123: `loadAdminUsers` can get stuck in an infinite retry loop
because `lastLoadedHref` is only updated on the success path, so failed loads
never satisfy the render guard that compares `currentHref` to `lastLoadedHref`.
Update `lastLoadedHref` for every terminal outcome in `loadAdminUsers`—including
the 403 branch and the catch path—so the loader doesn’t re-queue after a
failure. Keep the existing success behavior, and make sure the render condition
that checks `status === 'loading' || currentHref !== lastLoadedHref` can settle
after any completed request.

In `@packages/worker/client/routes/community-detail.tsx`:
- Around line 96-139: The community detail loader can retry forever because
loadCommunityDetail only updates lastLoadedListingId on success, leaving 404 and
catch paths to re-trigger the render guard in the community-detail route. Update
loadCommunityDetail so terminal failures also stamp lastLoadedListingId (or
otherwise mark the listing as handled) in the 404 branch and the catch block,
keeping the retry check from re-queuing the same listing. Use the existing
identifiers loadCommunityDetail, lastLoadedListingId, and the render condition
around listingId !== lastLoadedListingId to locate the fix.

---

Nitpick comments:
In `@e2e/ssr-hydration.spec.ts`:
- Around line 56-59: The URL assertion in ssr-hydration.spec.ts uses dynamic
RegExp construction with alphaListing.listingId, which triggers static analysis
even though the value is a fixed test constant. Update the page.toHaveURL check
in the relevant test to avoid constructing the pattern dynamically, and use a
safer direct assertion or a static regex shape in the assert around alphaListing
and its listingId.

In `@packages/worker/client/loader-data-types.ts`:
- Around line 77-96: `AccountIntegrationListItem.flow` is duplicating the shared
`IntegrationFlow` contract, which can drift from the route/component type
definitions. Update `AccountIntegrationListItem` in `loader-data-types.ts` to
reuse the existing `IntegrationFlow` type instead of the inline union, keeping
the loader shape aligned with `routes/account-integrations.tsx` and the shared
API contract.

In `@packages/worker/client/routes/account-integrations.tsx`:
- Around line 150-183: The router-hydration logic is duplicated across multiple
route components and has already drifted, so extract the shared
navigation/loading/embedded-data fallback flow into a reusable helper. Move the
repeated `listenToRouterNavigation`, `applyEmbeddedLoaderData`, and
`queueTask(loadIntegrations)` pattern into a common utility such as
`client-router.tsx` or `loader-data-context.tsx`, then update `account.tsx`,
`account-package-invocation-tokens.tsx`, `account-remote-connectors.tsx`,
`account-secrets.tsx`, `community.tsx`, and `admin-community-reports.tsx` to
call it. Keep the existing `handle`, `readCurrentRouterHref`,
`tryConsumeEmbeddedLoaderData`, and `loadIntegrations` behavior intact while
centralizing the `status` transition and embedded-data check.

In `@packages/worker/client/routes/admin-roles.tsx`:
- Around line 20-24: The isAdminRolesPath helper duplicates the same pathname
parsing logic found in other route files, so extract the shared pattern into a
common utility such as pathnameOf(href) and/or matchesPath, then update this
file and the similar helpers in admin-users.tsx and
account-integrations.tsx/account-remote-connectors.tsx to use it. Keep the
route-specific path comparison in each caller, but centralize the
URL-to-pathname normalization to remove repeated new URL(href,
'http://localhost').pathname logic.

In `@packages/worker/client/routes/login.tsx`:
- Around line 47-64: The auth helper logic in LoginRoute is bypassing the safer
router wrappers by calling readRouterPathname and readRouterSearch directly in
getCurrentAuthMode and getCurrentRedirectTo. Update these helpers to use the
existing getPathname-style fallback approach (or equivalent safe wrappers from
client-router.tsx) so pathname/search access stays consistent and resilient if
reused outside the router tree.

In `@packages/worker/src/app/account-secrets-data.ts`:
- Around line 320-431: The loader-data types are duplicated in this module
instead of reusing the shared exports from loader-data.ts, which can drift over
time. Update account-secrets-data.ts to import and use the existing shared
shapes for AccountSecretListItem, AccountSecretDetail, and the analogous
SecretApprovalView where applicable, and align any matching code in
toAccountSecretListItem and related helpers to those shared types. Also
consolidate the trailing export statements at the end of the file into a single
export block.
- Around line 77-88: `readSelectedSecretIdFromPath` contains a dead validation
branch because `parsedLegacyId` is computed but never changes the returned
value. Either remove the unused `parseAccountSecretId` check and simplify the
legacy path handling to return the sliced legacy id or null, or, if invalid
legacy ids should be rejected, change the `parseAccountSecretId` result to
actually gate the return in the `readSelectedSecretIdFromPath` function.

In `@packages/worker/src/app/data-cache.ts`:
- Around line 65-78: getOrSetDataCache currently only caches completed values,
so concurrent misses for the same key all call load() independently. Update
getOrSetDataCache to de-duplicate in-flight loads by tracking a per-key pending
promise in the data-cache module and having later callers await it instead of
starting a new load. Keep the existing cache hit/miss behavior intact and clear
the in-flight entry after the promise settles; use getOrSetDataCache,
peekDataCache, and setDataCache as the main touchpoints.

In `@packages/worker/src/app/handlers/admin-roles.ts`:
- Around line 16-27: The admin role handler currently performs a redundant
authentication fallback after requireUserWithRole(request, env, 'admin'). Remove
the extra readAuthSessionResult(request) and redirectToLogin(request) branch
from the admin-roles handler, since requireUserWithRole already handles
unauthenticated requests; keep the existing Response passthrough and error
rethrow behavior intact.

In `@packages/worker/src/app/handlers/auth-page.ts`:
- Around line 11-36: createAuthPageHandler currently calls renderAppPage({
request, env }) without a page title, so both createLoginHandler and
createSignupHandler end up using the same default SsrDocument title. Update the
auth-page flow to pass a distinguishing title through renderAppPage for each
route, using the createAuthPageHandler / createLoginHandler /
createSignupHandler entry points to select the appropriate login or signup title
while keeping the existing redirect and session logic unchanged.

In `@packages/worker/src/app/request-auth-cache.ts`:
- Around line 80-89: The role/permission lookup in request-auth-cache.ts only
logs to console when getUserRolesAndPermissions fails, so add an observable
signal there such as a metric/counter or structured app logger entry in the
catch block around the getUserRolesAndPermissions call. Use the existing request
auth path in request-auth-cache.ts and the getUserRolesAndPermissions flow to
record lookup failures while preserving the current fail-closed empty
roles/permissions fallback.

In `@packages/worker/src/app/request-cache.ts`:
- Around line 18-29: The request cache metadata currently stores only one
DataCacheLookup via setRequestDataCacheLookup, so later loaders can overwrite
earlier cache hits/misses before getRequestDataCacheLookup reads them back for
the X-Kody-Cache header. Update the requestMetaStore-backed request metadata to
accumulate or merge multiple lookups per request instead of replacing the
previous value, and adjust getRequestDataCacheLookup and any header-building
code to consume the combined result; use setRequestDataCacheLookup and
getRequestDataCacheLookup as the key entry points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0b9665d-ce33-4100-9292-4edcb2f03ee3

📥 Commits

Reviewing files that changed from the base of the PR and between ed933a5 and 2a5ef2d.

📒 Files selected for processing (86)
  • e2e/admin-rbac.spec.ts
  • e2e/d1-utils.ts
  • e2e/smoke.spec.ts
  • e2e/ssr-hydration.spec.ts
  • packages/worker/client/app-root.tsx
  • packages/worker/client/app.tsx
  • packages/worker/client/client-router.tsx
  • packages/worker/client/entry.tsx
  • packages/worker/client/loader-data-context.node.test.ts
  • packages/worker/client/loader-data-context.tsx
  • packages/worker/client/loader-data-types.ts
  • packages/worker/client/router-location.tsx
  • packages/worker/client/routes/account-integrations.tsx
  • packages/worker/client/routes/account-package-invocation-tokens.tsx
  • packages/worker/client/routes/account-remote-connectors.tsx
  • packages/worker/client/routes/account-secrets.tsx
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/admin-community-reports.tsx
  • packages/worker/client/routes/admin-roles.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/client/routes/community-detail.tsx
  • packages/worker/client/routes/community.tsx
  • packages/worker/client/routes/login.tsx
  • packages/worker/client/routes/oauth-authorize.tsx
  • packages/worker/client/routes/oauth-callback.tsx
  • packages/worker/client/routes/reset-password.tsx
  • packages/worker/project.json
  • packages/worker/src/app/account-integrations-data.ts
  • packages/worker/src/app/account-package-invocation-tokens-data.ts
  • packages/worker/src/app/account-profile-data.ts
  • packages/worker/src/app/account-remote-connectors-data.ts
  • packages/worker/src/app/account-secrets-data.ts
  • packages/worker/src/app/admin-community-reports-data.ts
  • packages/worker/src/app/admin-roles-data.ts
  • packages/worker/src/app/admin-users-data.ts
  • packages/worker/src/app/authenticated-user.ts
  • packages/worker/src/app/client-build-id.ts
  • packages/worker/src/app/community-api.ts
  • packages/worker/src/app/community-data.ts
  • packages/worker/src/app/data-cache.node.test.ts
  • packages/worker/src/app/data-cache.ts
  • packages/worker/src/app/handlers/account-integrations.node.test.ts
  • packages/worker/src/app/handlers/account-integrations.ts
  • packages/worker/src/app/handlers/account-package-invocation-tokens.node.test.ts
  • packages/worker/src/app/handlers/account-package-invocation-tokens.ts
  • packages/worker/src/app/handlers/account-profile.ts
  • packages/worker/src/app/handlers/account-remote-connectors.node.test.ts
  • packages/worker/src/app/handlers/account-remote-connectors.ts
  • packages/worker/src/app/handlers/account-secrets.node.test.ts
  • packages/worker/src/app/handlers/account-secrets.ts
  • packages/worker/src/app/handlers/account.ts
  • packages/worker/src/app/handlers/admin-community-reports.ts
  • packages/worker/src/app/handlers/admin-roles.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/handlers/auth-page.ts
  • packages/worker/src/app/handlers/community-detail.node.test.ts
  • packages/worker/src/app/handlers/community-detail.tsx
  • packages/worker/src/app/handlers/community.node.test.ts
  • packages/worker/src/app/handlers/community.ts
  • packages/worker/src/app/handlers/community.tsx
  • packages/worker/src/app/handlers/connect-oauth.ts
  • packages/worker/src/app/handlers/home.ts
  • packages/worker/src/app/handlers/login.ts
  • packages/worker/src/app/handlers/privacy.ts
  • packages/worker/src/app/handlers/reset-password.ts
  • packages/worker/src/app/handlers/session.ts
  • packages/worker/src/app/handlers/signup.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/app/request-auth-cache.ts
  • packages/worker/src/app/request-cache.ts
  • packages/worker/src/app/router.ts
  • packages/worker/src/app/routes.ts
  • packages/worker/src/app/session-info.ts
  • packages/worker/src/app/ssr-document.tsx
  • packages/worker/src/app/ssr-render.node.test.ts
  • packages/worker/src/app/ssr-render.tsx
  • packages/worker/src/app/ssr-stubs/app-root.ts
  • packages/worker/src/community/service.ts
  • packages/worker/src/index.ts
  • packages/worker/src/oauth-handlers.ts
  • packages/worker/tsconfig-client.json
  • packages/worker/tsconfig-worker-typecheck.json
  • packages/worker/tsconfig-worker.json
  • packages/worker/tsconfig.json
  • packages/worker/wrangler.jsonc
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • packages/worker/src/app/handlers/community.ts

Comment thread e2e/d1-utils.ts Resolved
Comment thread packages/worker/src/app/data-cache.ts Resolved
Comment thread packages/worker/client/app.tsx Resolved
…-consume embedded loader data, normalize report status filter, bound data cache, version styles.css, fix e2e seed upsert
… during refetch

Remove the currentHref === lastLoadedHref guard from grid render conditions
in community and admin-community-reports routes. The navigation listener
already sets status to loading on href changes, so stale-while-revalidate
behavior keeps existing listings visible during refetch instead of hiding
the grid after hydration when hrefs momentarily diverge.
When a session cookie references a deleted or missing user, protected HTML
handlers now include the session-destroy Set-Cookie on redirect-to-login
responses. The login page also validates the user via loadSessionInfo instead
of trusting the raw session cookie, so stale cookies cannot bounce visitors
back to protected routes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
e2e/d1-utils.ts (1)

128-134: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Other derived columns still go stale on reseed.

owner_user_id was correctly added to DO UPDATE SET here, fixing the previously flagged staleness issue. However, package_id, source_id, kody_id, license, and pinned_commit are still excluded from the update clause. If a listing is reseeded with a different packageId/sourceId/kodyId (or those derived defaults change based on input.listingId), these columns will remain stale on conflict, same root cause as the earlier owner_user_id bug.

🛠️ Proposed fix
 ON CONFLICT(id) DO UPDATE SET
 	owner_user_id = excluded.owner_user_id,
+	package_id = excluded.package_id,
+	source_id = excluded.source_id,
+	kody_id = excluded.kody_id,
 	name = excluded.name,
 	description = excluded.description,
 	tags_json = excluded.tags_json,
+	license = excluded.license,
+	pinned_commit = excluded.pinned_commit,
 	status = excluded.status,
 	updated_at = CURRENT_TIMESTAMP;`.trim()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@e2e/d1-utils.ts` around lines 128 - 134, The UPSERT in the D1 seeding logic
still leaves derived listing columns stale on conflict. Update the existing ON
CONFLICT(id) DO UPDATE SET clause in the d1-utils seeding SQL to also refresh
package_id, source_id, kody_id, license, and pinned_commit alongside
owner_user_id, name, description, tags_json, status, and updated_at. Keep the
change in the same seed/listing insert path so reseeding with a different
listingId or derived defaults always overwrites every mutable listing field.
🧹 Nitpick comments (1)
packages/worker/src/app/handlers/account.node.test.ts (1)

20-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test-env boilerplate duplicated across three test files.

createStaleSessionTestEnv() and testCookieSecret are copied verbatim into auth-page.node.test.ts and auth-redirect.node.test.ts. Consider extracting to a shared test helper module to avoid drift as the D1 mock evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/account.node.test.ts` around lines 20 - 56,
The same stale-session test setup is duplicated in multiple auth test files,
which risks the D1 mock drifting over time. Extract createStaleSessionTestEnv()
and the shared testCookieSecret into a reusable helper module, then update
account.node.test.ts, auth-page.node.test.ts, and auth-redirect.node.test.ts to
import and use that helper instead of maintaining separate copies.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/app/handlers/account.node.test.ts`:
- Line 1: The account.node.test.ts file imports vitest symbols, but the worker
package manifest does not declare vitest as a dependency. Update the
packages/worker package.json used by the worker workspace to add vitest so the
test imports resolve correctly; make sure the dependency is available to the
test suite that exercises the account.node.test.ts handler.

---

Outside diff comments:
In `@e2e/d1-utils.ts`:
- Around line 128-134: The UPSERT in the D1 seeding logic still leaves derived
listing columns stale on conflict. Update the existing ON CONFLICT(id) DO UPDATE
SET clause in the d1-utils seeding SQL to also refresh package_id, source_id,
kody_id, license, and pinned_commit alongside owner_user_id, name, description,
tags_json, status, and updated_at. Keep the change in the same seed/listing
insert path so reseeding with a different listingId or derived defaults always
overwrites every mutable listing field.

---

Nitpick comments:
In `@packages/worker/src/app/handlers/account.node.test.ts`:
- Around line 20-56: The same stale-session test setup is duplicated in multiple
auth test files, which risks the D1 mock drifting over time. Extract
createStaleSessionTestEnv() and the shared testCookieSecret into a reusable
helper module, then update account.node.test.ts, auth-page.node.test.ts, and
auth-redirect.node.test.ts to import and use that helper instead of maintaining
separate copies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fefd1886-db6f-4433-b8da-c5e03d42d878

📥 Commits

Reviewing files that changed from the base of the PR and between b7ecf6e and 54f1338.

📒 Files selected for processing (32)
  • e2e/d1-utils.ts
  • packages/worker/client/app.tsx
  • packages/worker/client/loader-data-context.tsx
  • packages/worker/client/routes/account-integrations.tsx
  • packages/worker/client/routes/account-package-invocation-tokens.tsx
  • packages/worker/client/routes/account-remote-connectors.tsx
  • packages/worker/client/routes/account-secrets.tsx
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/admin-community-reports.tsx
  • packages/worker/client/routes/admin-roles.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/client/routes/community-detail.tsx
  • packages/worker/client/routes/community.tsx
  • packages/worker/src/app/admin-community-reports-data.ts
  • packages/worker/src/app/auth-redirect.node.test.ts
  • packages/worker/src/app/auth-redirect.ts
  • packages/worker/src/app/client-build-id.ts
  • packages/worker/src/app/data-cache.node.test.ts
  • packages/worker/src/app/data-cache.ts
  • packages/worker/src/app/handlers/account-integrations.ts
  • packages/worker/src/app/handlers/account-package-invocation-tokens.ts
  • packages/worker/src/app/handlers/account-remote-connectors.ts
  • packages/worker/src/app/handlers/account-secrets.ts
  • packages/worker/src/app/handlers/account.node.test.ts
  • packages/worker/src/app/handlers/account.ts
  • packages/worker/src/app/handlers/auth-page.node.test.ts
  • packages/worker/src/app/handlers/auth-page.ts
  • packages/worker/src/app/handlers/package-app.ts
  • packages/worker/src/app/permissions-server.ts
  • packages/worker/src/app/ssr-document.tsx
  • packages/worker/src/app/ssr-render.tsx
  • packages/worker/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • packages/worker/src/app/handlers/auth-page.ts
  • packages/worker/src/app/ssr-render.tsx
  • packages/worker/client/routes/account-integrations.tsx
  • packages/worker/client/loader-data-context.tsx
  • packages/worker/src/app/handlers/account.ts
  • packages/worker/src/app/admin-community-reports-data.ts
  • packages/worker/src/app/ssr-document.tsx
  • packages/worker/client/routes/account.tsx
  • packages/worker/client/routes/community-detail.tsx
  • packages/worker/src/app/data-cache.ts
  • packages/worker/client/routes/admin-roles.tsx
  • packages/worker/client/routes/account-remote-connectors.tsx
  • packages/worker/client/app.tsx
  • packages/worker/src/index.ts
  • packages/worker/src/app/handlers/account-remote-connectors.ts
  • packages/worker/client/routes/community.tsx
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/src/app/handlers/account-package-invocation-tokens.ts
  • packages/worker/src/app/handlers/account-integrations.ts
  • packages/worker/client/routes/admin-community-reports.tsx
  • packages/worker/client/routes/account-secrets.tsx
  • packages/worker/src/app/handlers/account-secrets.ts

@@ -0,0 +1,82 @@
import { expect, test, vi } from 'vitest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -a package.json packages/worker | xargs grep -n '"vitest"'

Repository: kentcdodds/kody

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- package.json files ---'
git ls-files '**/package.json' | sed -n '1,200p'

printf '\n%s\n' '--- vitest declarations ---'
for f in $(git ls-files '**/package.json'); do
  if grep -n '"vitest"' "$f" >/dev/null 2>&1; then
    echo "FILE: $f"
    grep -n '"vitest"' "$f"
  fi
done

Repository: kentcdodds/kody

Length of output: 313


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' packages/worker/package.json

Repository: kentcdodds/kody

Length of output: 1221


Add vitest to packages/worker/package.json. The test file imports it, but no workspace manifest declares it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/account.node.test.ts` at line 1, The
account.node.test.ts file imports vitest symbols, but the worker package
manifest does not declare vitest as a dependency. Update the packages/worker
package.json used by the worker workspace to add vitest so the test imports
resolve correctly; make sure the dependency is available to the test suite that
exercises the account.node.test.ts handler.

Extract authorize-info computation into loadOAuthAuthorizeData shared by
the JSON endpoint and GET /oauth/authorize HTML. Embed the result under
oauthAuthorize in AppLoaderData so streamed HTML shows the client name,
scopes, or error state instead of the loading branch until hydration.

Consume embedded data in oauth-authorize.tsx via tryConsumeEmbeddedLoaderData
before falling back to client fetch, matching the community route pattern.
Run embedded-data consumption and initial loadInfo when status is idle,
not only when the search string changes, so empty-query authorize pages
still surface server or client error state.

Stop overwriting message from URL error query params after a successful
embedded oauthAuthorize payload; match loadInfo behavior that clears
message on success.
Add a typed frame registry (registerFrame, handleFrameRequest,
resolveRegisteredFrameHtml) shared by SSR and route handlers. Promote
x-remix-target to frame-constants.ts, migrate the frame-spike route to
the new helper, and enable client imports of routes.href() for Frame src.

Add kody-custom/no-literal-frame-src oxlint rule and docs/contributing/frames.md.
…x-ssr-hydration-c537

# Conflicts:
#	packages/worker/client/loader-data-types.ts
#	packages/worker/tsconfig-client.json

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 53915ea. Configure here.

@kentcdodds
kentcdodds merged commit d5a6147 into main Jul 4, 2026
5 checks passed
@kentcdodds
kentcdodds deleted the cursor/remix-ssr-hydration-c537 branch July 4, 2026 17:57
cursor Bot pushed a commit that referenced this pull request Jul 4, 2026
…st-scoped detail loads, throttled session refresh

Post-merge follow-up to the Remix 3 SSR work (#601):

- handleRequest builds the app router and parses env once per env object
  identity instead of on every request (~40 handler factories + schema
  validation were re-run per request).
- loadAdminUsersData issues the COUNT and page queries in parallel.
- loadCommunityDetailData memoizes per Request so the HTML handler and
  the frame renderer share one load during a single SSR response.
- Navigation-triggered client session refreshes are throttled to 30s;
  hydration and explicit refreshes (login/logout/profile) still always
  hit /session.

Co-authored-by: Kent C. Dodds <me+github@kentcdodds.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants