Skip to content

Foldkit

Foldkit is an Elm-architecture frontend framework built on Effect. Its apps are client-only Vite projects — the Foldkit Vite plugin only adds HMR and devtools wiring — so Railway.Website.Foldkit is Railway.Website.Vite with SPA fallback to index.html. Deep links boot the app and the Foldkit router takes over.

Deploy creates a Railway.Project (unless you pass project) and a Railway.Service from a container image. The URL is https://{name}.up.railway.app.

The build integration is not bundled with alchemy. Install @alchemy.run/frontend-frameworks; the resource loads its /vite and /vite/node exports from your project at deploy time. It is only used at build time, so a dev dependency is enough:

Terminal window
bun add -d @alchemy.run/frontend-frameworks

Your Vite config stays what Foldkit’s setup gives you. Vite plugins like Tailwind go in plugins as usual:

vite.config.ts
import { foldkit } from "@foldkit/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [foldkit(), tailwindcss()],
optimizeDeps: {
entries: ["src/entry.ts"],
},
});

Alchemy runs Vite programmatically on the project root. The Foldkit plugin and the rest of your setup are preserved as-is.

Declare the site as a module-level const (rather than inline in the Stack):

alchemy.run.ts
import * as Railway from "alchemy/Railway";
export const Website = Railway.Website.Foldkit("Website");

For an app in a subdirectory of a monorepo, point rootDir at it:

export const Website = Railway.Website.Foldkit("Website", {
rootDir: "applications/web",
});

Yield the site from your Stack and return its URL:

alchemy.run.ts
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
export default Alchemy.Stack(
"MyFoldkitSite",
{
providers: Railway.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
const site = yield* Website;
return { url: site.url };
}),
);

site.url is the generated https://{name}.up.railway.app. The site also exposes service and project — they are undefined under alchemy dev.

A Foldkit SPA has no server of its own, so anything it needs from the rest of your Stack is baked into the bundle at build time — pass a VITE_-prefixed key in top-level env. Values must be strings (or Redacted); resolve other resources’ URLs before passing them.

alchemy.run.ts
export const Website = Railway.Website.Foldkit("Website", {
env: {
VITE_API_URL: "https://api.example.com",
},
});

The values are copied onto process.env before the Vite build and onto the Service. Client code reads the inlined value:

src/vite-env.d.ts
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
const apiUrl = import.meta.env.VITE_API_URL;

A Foldkit app that uses URL routing (Runtime.makeApplication with route, onUrlRequest, and onUrlChange) resolves routes on the client, so a deep link like /counter/42 arrives as a request for a file that doesn’t exist.

Foldkit defaults assets.notFoundHandling to "single-page-application", which returns index.html (200) for unmatched paths so the Foldkit runtime can resolve the route. Use "404-page" to serve a real 404 instead:

export const Website = Railway.Website.Foldkit("Website", {
assets: { notFoundHandling: "404-page" },
});

alchemy dev runs Vite’s own dev server (native HMR, Foldkit devtools) instead of deploying; site.url is the local address and no Project or Service is created. Wrap the site in Alchemy.remote() to deploy the live Service even during dev.

Pin the dev server’s address when several apps run in one Stack:

export const Website = Railway.Website.Foldkit("Website", {
dev: { host: "127.0.0.1", port: 5180, strictPort: true },
});

Pass a hostname to attach a Railway.CustomDomain (targetPort 3000). url becomes https://{domain}.

const site = yield* Railway.Website.Foldkit("Web", {
domain: "app.example.com",
});