1---
2title: Waku
3description: Deploy a Waku app to Fly with Fly.Website.Waku — RSC server on a Machine, SSG pages baked into 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`Fly.Website.Waku` deploys a [Waku](https://waku.gg) app (React
13Server Components) to Fly. It builds the project programmatically:
14the RSC server runs as a Node [`Fly.Service`](/fly/compute/services)
15on a Machine, and the client output — including SSG-prerendered
16pages — is baked into the image. Omit `app` and Alchemy creates a
17[`Fly.App`](/fly/compute/apps) plus a shared IPv4 so
18`https://{app}.fly.dev` answers. No `waku.config.ts` edits are
19required — if you have one it loads natively — and there is no
20adapter to configure and no build command to 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 Fly from "alchemy/Fly";
82
83export const Website = Fly.Website.Waku("Website");
84```
85
86Pass `app` to put the site on an existing `Fly.App`. `rootDir`
87defaults 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: Fly.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 Machine.
113Prerendered pages are served at their extensionless URLs (`/about`).
114
115The live `url` is `https://{app}.fly.dev`. `site.app`,
116`site.service`, `site.ip`, and `site.certificate` are the Fly
117resources underneath — `undefined` during `alchemy dev`.
118
119See the [Waku API reference](/providers/fly/website/waku) for every
120prop 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 = Fly.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 Machine 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 Fly resources are
165created. Wrap the site in `Alchemy.remote()` to deploy the live
166App and Service even during dev:
167
168```typescript
169export const Website = Fly.Website.Waku("Website").pipe(
170 Alchemy.remote(),
171);
172```
173
174## Custom domain
175
176```typescript
177const site = yield* Fly.Website.Waku("Web", {
178 domain: "app.example.com",
179});
180```
181
182`domain` requests ACME (`Fly.Certificate`) on the App. `url`
183becomes `https://app.example.com`. Point DNS at the App first — v1
184does not create DNS records.
185
186## Where next
187
188- [Waku API reference](/providers/fly/website/waku)
189- [Apps](/fly/compute/apps) and [Services](/fly/compute/services)
190- [IPs & certificates](/fly/networking)