1---
2title: Waku
3description: Deploy a Waku app to Railway with Railway.Website.Waku — RSC server on a container Service, SSG pages in the image, and Waku's own dev server under alchemy dev.
4sidebar:
5 order: 13
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12`Railway.Website.Waku` deploys a [Waku](https://waku.gg) app (React
13Server Components) to Railway. It builds the project programmatically:
14the RSC server plus SSG pages run on one
15[`Railway.Service`](/railway/compute/services). Alchemy creates a
16[`Railway.Project`](/railway/compute/projects) if you omit
17`project`. The URL is the generated `*.up.railway.app` hostname. No
18`waku.config.ts` edits are required — if you have one it loads
19natively — and there is no adapter to configure and no build command
20to run.
21
22## Install
23
24The build integration is not bundled with alchemy. Install
25`@alchemy.run/frontend-frameworks`; the resource loads its `/waku`
26and `/waku/node` exports from your project at deploy time. It is only
27used at build time, so a dev dependency is enough:
28
29<Tabs syncKey="pkgManager">
30 <TabItem label="bun" icon="bun">
31 <Code code={`bun add -d ${devDeps}`} lang="sh" />
32 </TabItem>
33 <TabItem label="npm" icon="npm">
34 <Code code={`npm install -D ${devDeps}`} lang="sh" />
35 </TabItem>
36 <TabItem label="pnpm" icon="pnpm">
37 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
38 </TabItem>
39 <TabItem label="yarn" icon="seti:yarn">
40 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
41 </TabItem>
42</Tabs>
43
44## Configure Waku
45
46No framework config is required — a fresh Waku project deploys
47as-is. If your project has a `waku.config.ts`, Alchemy loads it
48natively (exactly as Waku's own CLI does) and uses it as the base
49config — including Vite plugins under its `vite` field:
50
51```typescript
52// waku.config.ts
53import tailwindcss from "@tailwindcss/vite";
54import { defineConfig } from "waku/config";
55
56export default defineConfig({
57 basePath: "/",
58 vite: {
59 plugins: [tailwindcss()],
60 },
61});
62```
63
64Options set on the resource (`srcDir`, `distDir`, `basePath`) merge
65over the file, per key. The one key Alchemy owns is
66`unstable_adapter` — the Node deploy target sets it. Setting it
67fails the build with an actionable error.
68
69:::note
70Vite configuration must live in `waku.config.ts`'s `vite` field —
71a standalone `vite.config.ts` is not loaded, same as Waku's own CLI.
72:::
73
74## Declare the Website
75
76Declare the site as a module-level const (rather than inline in the
77Stack):
78
79```typescript
80// alchemy.run.ts
81import * as Railway from "alchemy/Railway";
82
83export const Website = Railway.Website.Waku("Website");
84```
85
86Pass `project` to put the Service in an existing `Railway.Project`.
87`rootDir` defaults to `"."`.
88
89## Add it to the Stack
90
91Yield the site from your Stack and return its URL:
92
93```typescript
94// alchemy.run.ts
95import * as Alchemy from "alchemy";
96import * as Effect from "effect/Effect";
97
98export default Alchemy.Stack(
99 "MyWakuSite",
100 {
101 providers: Railway.providers(),
102 state: Alchemy.localState(),
103 },
104 Effect.gen(function* () {
105 const site = yield* Website;
106 return { url: site.url };
107 }),
108);
109```
110
111Pages rendered as `"static"` are generated at build time and baked
112into the image; dynamic pages and API routes run on the Service.
113SSG pages are served at their extensionless URLs (`/about`).
114
115The live `url` is `https://{name}.up.railway.app`. `site.service`
116and `site.project` are the Railway resources underneath —
117`undefined` during `alchemy dev`.
118
119See the [Waku API reference](/providers/railway/website/waku) for
120every prop and attribute.
121
122## Add environment variables
123
124Process environment is a top-level `env` map — plain strings, or
125`Redacted` values:
126
127```diff lang="typescript"
128// alchemy.run.ts
129export const Website = Railway.Website.Waku("Website", {
130+ env: {
131+ GREETING: "Hello from alchemy",
132+ API_BASE: "https://api.example.com",
133+ },
134});
135```
136
137The values are copied onto `process.env` before the build and the
138dev server, and onto the Service at deploy time, so server code
139reads the same keys in both modes. Client inlining is whatever Vite
140does (`VITE_` prefixes).
141
142## Read the environment in server code
143
144The RSC server is a plain Node process. Server components and API
145routes read `process.env` (Waku's `getEnv` reads the same values):
146
147```typescript
148// src/pages/index.tsx
149export default async function HomePage() {
150 const greeting = process.env.GREETING ?? "hello";
151 return <h1>{greeting}</h1>;
152}
153
154export const getConfig = async () => ({ render: "dynamic" }) as const;
155```
156
157## Local dev
158
159```sh
160bun alchemy dev
161```
162
163`alchemy dev` runs Waku's own dev server (native HMR) instead of
164deploying; `site.url` is the local address and no Railway resources
165are created. Wrap the site in
166`Alchemy.remote()` to deploy the live Service even during dev:
167
168```typescript
169export const Website = Railway.Website.Waku("Website").pipe(Alchemy.remote());
170```
171
172## Custom domain
173
174```typescript
175const site = yield* Railway.Website.Waku("Web", {
176 domain: "app.example.com",
177});
178```
179
180`domain` attaches a [`Railway.CustomDomain`](/railway/networking)
181(`targetPort` 3000). `url` becomes `https://app.example.com`
182instead of the generated `*.up.railway.app`.
183
184## Where next
185
186- [Waku API reference](/providers/railway/website/waku)
187- [Projects](/railway/compute/projects) and
188 [Services](/railway/compute/services)
189- [Custom domains](/railway/networking)