1---
2title: Foldkit
3description: Deploy a Foldkit app to Hetzner with Hetzner.Website.Foldkit — a client-only Vite SPA as a systemd unit on a Cloud Server, and Vite's own dev server under alchemy dev.
4sidebar:
5 order: 8
6---
7
8import { Code, Tabs, TabItem } from "@astrojs/starlight/components";
9
10export const devDeps = "@alchemy.run/frontend-frameworks";
11
12[Foldkit](https://foldkit.dev) is an Elm-architecture frontend
13framework built on Effect. Its apps are client-only Vite projects —
14the Foldkit Vite plugin only adds HMR and devtools wiring — so
15`Hetzner.Website.Foldkit` is
16[`Hetzner.Website.Vite`](/hetzner/frontend/vite) with SPA fallback
17to `index.html`. Deep links boot the app and the Foldkit router
18takes over.
19
20Deploy creates a [`Hetzner.Server`](/hetzner/compute/servers)
21(`cpx12` / `ubuntu-24.04` in `fsn1`, unless you pass `server`) and a
22[`Hetzner.Service`](/hetzner/compute/services) systemd unit on port
233000. The live URL is `http://{ipv4}:3000` — there is no TLS on the
24Service. Pass `server` to share the VM with other sites and APIs.
25
26## Install
27
28The build integration is not bundled with alchemy. Install
29`@alchemy.run/frontend-frameworks`; the resource loads its `/vite`
30and `/vite/node` exports from your project at deploy time. It is only
31used at build time, so a dev dependency is enough:
32
33<Tabs syncKey="pkgManager">
34 <TabItem label="bun" icon="bun">
35 <Code code={`bun add -d ${devDeps}`} lang="sh" />
36 </TabItem>
37 <TabItem label="npm" icon="npm">
38 <Code code={`npm install -D ${devDeps}`} lang="sh" />
39 </TabItem>
40 <TabItem label="pnpm" icon="pnpm">
41 <Code code={`pnpm add -D ${devDeps}`} lang="sh" />
42 </TabItem>
43 <TabItem label="yarn" icon="seti:yarn">
44 <Code code={`yarn add -D ${devDeps}`} lang="sh" />
45 </TabItem>
46</Tabs>
47
48## Configure Vite
49
50Your Vite config stays what Foldkit's setup gives you. Vite plugins
51like Tailwind go in `plugins` as usual:
52
53```typescript
54// vite.config.ts
55import { foldkit } from "@foldkit/vite-plugin";
56import tailwindcss from "@tailwindcss/vite";
57import { defineConfig } from "vite";
58
59export default defineConfig({
60 plugins: [foldkit(), tailwindcss()],
61 optimizeDeps: {
62 entries: ["src/entry.ts"],
63 },
64});
65```
66
67Alchemy runs Vite programmatically on the project root. The Foldkit
68plugin and the rest of your setup are preserved as-is.
69
70:::note[Pinned Effect versions]
71Foldkit pins its `effect` peer dependency to an exact version.
72Alchemy runs your app's own Vite build with your app's own
73`node_modules`, so that pin is between Foldkit and your package
74manager — Alchemy imposes no Effect version on your frontend.
75:::
76
77## Declare the Website
78
79Declare the site as a module-level const (rather than inline in the
80Stack):
81
82```typescript
83// alchemy.run.ts
84import * as Hetzner from "alchemy/Hetzner";
85
86export const Website = Hetzner.Website.Foldkit("Website");
87```
88
89For an app in a subdirectory of a monorepo, point `rootDir` at it:
90
91```typescript
92export const Website = Hetzner.Website.Foldkit("Website", {
93 rootDir: "applications/web",
94});
95```
96
97## Add it to the Stack
98
99Yield the site from your Stack and return its URL:
100
101```typescript
102// alchemy.run.ts
103import * as Alchemy from "alchemy";
104import * as Effect from "effect/Effect";
105
106export default Alchemy.Stack(
107 "MyFoldkitSite",
108 {
109 providers: Hetzner.providers(),
110 state: Alchemy.localState(),
111 },
112 Effect.gen(function* () {
113 const site = yield* Website;
114 return { url: site.url };
115 }),
116);
117```
118
119`site.url` is `http://{ipv4}:3000`. The site also exposes `server`
120and `service` — they are `undefined` under `alchemy dev`.
121
122## Add environment variables
123
124A Foldkit SPA has no server of its own, so anything it needs from
125the rest of your Stack is baked into the bundle at build time — pass
126a `VITE_`-prefixed key in top-level `env`. Values must be strings (or
127`Redacted`); resolve other resources' URLs before passing them.
128
129```diff lang="typescript"
130// alchemy.run.ts
131export const Website = Hetzner.Website.Foldkit("Website", {
132+ env: {
133+ VITE_API_URL: "https://api.example.com",
134+ },
135});
136```
137
138The values are copied onto `process.env` before the Vite build and
139onto the systemd unit. Client code reads the inlined value:
140
141```typescript
142// src/vite-env.d.ts
143/// <reference types="vite/client" />
144
145interface ImportMetaEnv {
146 readonly VITE_API_URL: string;
147}
148```
149
150```typescript
151const apiUrl = import.meta.env.VITE_API_URL;
152```
153
154## Deep links
155
156A Foldkit app that uses URL routing (`Runtime.makeApplication` with
157`route`, `onUrlRequest`, and `onUrlChange`) resolves routes on the
158client, so a deep link like `/counter/42` arrives as a request for a
159file that doesn't exist.
160
161`Foldkit` defaults `assets.notFoundHandling` to
162`"single-page-application"`, which returns `index.html` (200) for
163unmatched paths so the Foldkit runtime can resolve the route. Use
164`"404-page"` to serve a real 404 instead:
165
166```typescript
167export const Website = Hetzner.Website.Foldkit("Website", {
168 assets: { notFoundHandling: "404-page" },
169});
170```
171
172## Local dev
173
174`alchemy dev` runs Vite's own dev server (native HMR, Foldkit
175devtools) instead of deploying; `site.url` is the local address and
176no Server or Service is created. Wrap the site in `Alchemy.remote()`
177to deploy the live unit even during dev.
178
179Pin the dev server's address when several apps run in one Stack:
180
181```typescript
182export const Website = Hetzner.Website.Foldkit("Website", {
183 dev: { host: "127.0.0.1", port: 5180, strictPort: true },
184});
185```
186
187## Custom domain
188
189`domain` requires an existing [`Hetzner.Zone`](/hetzner/networking/dns)
190— v1 does not create one. Alchemy adds an A record pointing at the
191Server's public IPv4. `url` becomes `http://{domain}:3000`. There is
192no TLS on the Service.
193
194```typescript
195const site = yield* Hetzner.Website.Foldkit("Web", {
196 domain: "app.example.com",
197 zone,
198});
199```
200
201## Where next
202
203- [`Foldkit` reference](/providers/hetzner/website/foldkit) — every
204 prop and attribute
205- [Vite](/hetzner/frontend/vite) — the same static-file unit, with
206 `assets.notFoundHandling`
207- [Websites](/hetzner/frontend/websites) — the rest of the Hetzner
208 frontend family
209- [Servers](/hetzner/compute/servers),
210 [Services](/hetzner/compute/services),
211 [DNS](/hetzner/networking/dns)