Remix guidance
September 14, 2026 · View on GitHub
Use the repo-local Remix skill instead of vendoring generated Remix package documentation in this repository.
The repo-local skill lives at:
.agents/skills/remix/SKILL.md
Load that skill before changing Remix routes, controllers, middleware, data access, validation, auth, sessions, file uploads, server setup, UI components, hydration, navigation, frames, or tests.
Kody D1 leftovers (unused columns, dual-write, a later drop) follow Cleanup after migrations, not the Remix skill.
For <Frame> partial reloads, see frames.
npx remix@next new <app> copies this skill from the Remix CLI bootstrap
template. The CLI does not expose a standalone remix skills install command.
Kody runtime topology
The Remix UI ships on the origin Cloudflare Worker (kody /
kody-production), not as the Node HTTP server generated by remix new. The
origin request path is:
packages/worker/src/index.ts -> packages/worker/src/app/handler.ts ->
packages/worker/src/app/router.ts
Package-app hosts (kody.run) and the invocation API live on kody-runtime.
Platform Durable Objects and scheduled jobs live on kody-platform and
kody-jobs. See architecture.
Package apps are framework-agnostic Worker entries on kody-runtime (dynamic
Worker Loader isolate), not on the origin. Remix is a recipe: the package's
app/router.ts default-exports a fetch handler that remounts into
createRouter(), the platform supplies remix at the same version as this repo
(pre-bundled by tools/build-worker-bundler-modules.ts, mounted at
node_modules/remix/ in the runtime bundler), and kody:runtime exposes Kody
as named exports plus the optional KodyRuntime request-context key. Authoring
conventions for that surface live in
docs/guides/package-apps.md; the platform side is
in packages and manifests. The skill
below applies to origin UI and to the Remix package-app recipe, with two
package-app differences: no Vite (esbuild via the runtime bundler, no HMR) and
no remix/assets (the browser entry is one platform-built module under
/_assets, hydrated through a loadModule registry).
Keep these Worker-specific differences when comparing Kody with the default template:
- Wrangler supplies canonical Fetch API
Requestobjects. Kody does not useremix/node-fetch-server, so its Node-onlytrustProxyoption does not apply. - Production ships four product scripts via GitHub Actions.
npm run deployuploads origin only; there is no long-runningnpm startprocess. - Development uses Vite (
@pitlane/dev+@cloudflare/vite-plugin) so origin SSR runs in workerd with HMR. Production client and origin worker assets come fromvite build. Platform, runtime, jobs, and highlight stay auxiliary workers invite devand separate Wrangler deploys in production. - Static files are served through the Workers Assets binding rather than
remix/assetsorremix/middleware/static. Hydration usesclientEntry(import.meta.url, …)and Pitlane?assets=imports. SSRrenderToStreammust passresolveClientEntryso the serialized entry metadata (<script id="rmx-data">) points at the Vite hashed entry (/assets/entry-*.js), not the deleted/client-entry.js. Vite resolves imports itself, so noimportMapis returned and the Remix import-map polyfill is not used. - Remix
run()falls back to full document navigation when the browser lacks the Navigation API; do not add awindow.navigationstub.crypto.randomUUIDand constructable stylesheets are still polyfilled inpackages/worker/client/entry.tsxfor older in-app browsers. - Frame resolution is configured in both
packages/worker/client/entry.tsxandpackages/worker/src/app/ssr-render.tsx. The browser resolver is(src, options)and returns theResponse. SSRresolveFrameis(src, target, context). - The app router uses Remix COP (
remix/middleware/cop) before the account write lease. MCP, OAuth, package apps, and connectors never see that stack becauseindex.tshandles them first. COP bypasses Stripe webhooks, package webhook ingress, and/sentry-tunnel. - App D1 access goes through
D1DatabaseDriver(new Database(driver)). Remix does not ship a D1 factory. - Session and signed cookies import
createCookiefromremix/cookie. - Usernames are DNS labels for
{username}.package-app hosts, so they cannot contain dots. Path params that may contain dots (secret names) must go throughhref()so.is encoded as%2E.
If a Node server entry point is added later, evaluate trustProxy only at that
trusted reverse-proxy boundary. Do not copy it into Worker request handling.
Adding a page
For a typical authenticated HTML page, start with the route contract and wire the narrowest owners. The core route-wiring path is five files:
packages/worker/universal/routes.ts— add the route identity. Treatroutes.<name>as the canonical source of the pathname.packages/worker/src/app/router.ts— map the route to its handler inrouter.map(...).packages/worker/src/app/handlers/<page>.ts— implement the page handler. Use the helpers in#app/page-auth.ts(requireAuthenticatedPageUser,requirePageSession,requirePageUserWithRole) for page auth, and use#app/request-body.tshelpers when the page reads structured request bodies.packages/worker/client/routes/<page>.tsx— add the route component and its route loader, and read the payload throughcreateRouteDatafrom#client/route-data.tsxso navigations replace the previous page in one commit with no loading state (see no-flash navigation). Import shared loader payload types from#universal/loader-data.ts(kody-custom/prefer-loader-data-typesenforces this). Keep UI-only state types local.packages/worker/client/routes/index.tsx— register the component inclientRoutesand any loader inclientRouteLoaders, keyed byroutePattern(routes.<name>)instead of a duplicated literal pathname.
That covers the server contract, page handler, client route, and client
registries in five edits. Treat packages/worker/universal/document-head.ts as
the 5-vs-6 check: the count stays at five when the page can reuse existing head
behavior, and it becomes six when the new pathname needs its own title,
canonical URL, or Open Graph metadata. A brand-new standalone route commonly
needs that sixth edit because unmatched pathnames fall back to the Not found
title.
OAuth authorize/callback shells are the exception to the routes.ts rule. Those
pathnames are owned by the Cloudflare OAuth provider wrapper, so client
registries and document head use #universal/oauth-paths.ts
(oauthPaths.authorize and oauthPaths.callback) instead of routes.ts.