React Router
Railway.Website.ReactRouter deploys a
React Router v7 app in framework mode to
Railway. React Router builds through Vite, so Alchemy runs your
project’s own build: the SSR server plus client assets run on one
Railway.Service. Alchemy creates a
Railway.Project if you omit
project. The URL is the generated *.up.railway.app hostname. No
adapter.
Install
Section titled “Install”The build integration is not bundled with alchemy. Install
@alchemy.run/frontend-frameworks; the resource loads its
/react-router and /react-router/node exports from your project at
deploy time. It is only used at build time, so a dev dependency is
enough:
bun add -d @alchemy.run/frontend-frameworksnpm install -D @alchemy.run/frontend-frameworkspnpm add -D @alchemy.run/frontend-frameworksyarn add -D @alchemy.run/frontend-frameworksThe React Router side is whatever npx create-react-router@latest
scaffolds — react-router, @react-router/dev, @react-router/node,
isbot, and vite. Keep @react-router/node and isbot in
dependencies: React Router picks its default entry.server by reading
them from your package.json, and installs isbot on the fly if it is
missing.
Configure Vite
Section titled “Configure Vite”Your vite.config.ts stays what the React Router scaffold gives
you — no adapter, no server preset. Vite plugins like Tailwind go in
plugins as usual:
import { reactRouter } from "@react-router/dev/vite";import tailwindcss from "@tailwindcss/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [tailwindcss(), reactRouter()],});reactRouter() owns routing, typegen, and both build environments.
Alchemy drives the same two passes react-router build runs, wraps the
server manifest with createRequestHandler, and packages the resulting
fetch handler as a Node HTTP server on port 3000.
Declare the Website
Section titled “Declare the Website”Declare the site as a module-level const (rather than inline in the Stack):
import * as Railway from "alchemy/Railway";
export const Website = Railway.Website.ReactRouter("Website");Pass project to put the Service in an existing Railway.Project.
rootDir is the project root — the directory holding package.json,
vite.config.*, and react-router.config.ts. It defaults to ".",
so set it only when the app does not sit next to alchemy.run.ts:
export const Website = Railway.Website.ReactRouter("Website", { rootDir: "./web",});Add it to the Stack
Section titled “Add it to the Stack”Yield the site from your Stack and return its URL:
import * as Alchemy from "alchemy";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "MyReactRouterSite", { providers: Railway.providers(), state: Alchemy.localState(), }, Effect.gen(function* () { const site = yield* Website; return { url: site.url }; }),);Requests matching a built client asset are served first; everything else — SSR routes, loaders, actions, resource routes — falls through to the Node handler on the Service.
The live url is https://{name}.up.railway.app. site.service
and site.project are the Railway resources underneath —
undefined during alchemy dev.
See the React Router API reference for every prop and attribute.
How the two build outputs are deployed
Section titled “How the two build outputs are deployed”react-router build writes two directories, and both land in the
Service image:
build/├── client/ -> baked into the image, served first└── server/ -> wrapped as a Node HTTP server on port 3000build/client holds the hashed browser bundles plus everything you put
in public/. build/server/index.js is a ServerBuild manifest, not a
request handler, so Alchemy wraps it with createRequestHandler during
the build and emits a Node serve entry — that process is the
Railway.Service.
If you set Vite’s base, set React Router’s basename to the same
prefix: the client directory maps onto the site root one-for-one, so the
paths the SSR document emits have to be the paths the image was given.
Add environment variables
Section titled “Add environment variables”Process environment is a top-level env map — plain strings, or
Redacted values:
export const Website = Railway.Website.ReactRouter("Website", { env: { API_BASE: "https://api.example.com", },});The values are copied onto process.env before the build and the
dev server, and onto the Service at deploy time, so server code
reads the same keys in both modes. Client inlining is whatever Vite
does (VITE_ prefixes).
An output from another resource only exists once that resource is yielded, so declare the site inside the Stack generator instead:
Effect.gen(function* () { const api = yield* Api;
const site = yield* Railway.Website.ReactRouter("Website", { env: { API_BASE: api.url }, });
return { url: site.url };});Read the environment in a loader
Section titled “Read the environment in a loader”The React Router server is a plain Node process. Loaders and
actions read the environment from process.env:
export function loader() { return { apiBase: process.env.API_BASE ?? "unset" };}Values prefixed VITE_ are inlined into the client bundle at build time
instead, as import.meta.env.VITE_* — set those in the shell that runs
alchemy deploy, not in env.
Read the environment in a resource route
Section titled “Read the environment in a resource route”A route module with no default export is a resource route, served by the same Node process:
export function loader() { return new Response(process.env.API_BASE ?? "unset");}Build configuration
Section titled “Build configuration”The build runs through your project’s own vite.config.ts and
react-router.config.ts. A custom buildDirectory just works — the
integration observes the directory from the resolved Vite config, so
there is nothing to mirror on the resource.
Local dev
Section titled “Local dev”bun alchemy devalchemy dev runs React Router’s own Vite dev server (native HMR)
instead of deploying; site.url is the local address and no Railway
resources are created. Wrap the site in
Alchemy.remote() to deploy the live Service even during dev:
export const Website = Railway.Website.ReactRouter("Website").pipe( Alchemy.remote(),);React Server Components
Section titled “React Server Components”RSC is not supported yet. React Router’s unstable RSC plugin — and
the future.v8_viteEnvironmentApi flag — emit more than one server
environment, and this resource assembles a single Node entry. A build
that produces no single server entry fails with an actionable error
rather than deploying something broken.
Custom domain
Section titled “Custom domain”const site = yield* Railway.Website.ReactRouter("Web", { domain: "app.example.com",});domain attaches a Railway.CustomDomain
(targetPort 3000). url becomes https://app.example.com
instead of the generated *.up.railway.app.