From zeroto running.

elm-ssr is one package. It ships the CLI, a Fetch-compatible SSR runtime, the Elm authoring modules, and a SQL migration runner. It runs wherever you can call a standard Request → Response handler.

Prerequisites

Install.

Requires Bun ≥ 1.3 and the Elm compiler. Wrangler is only needed if you target Cloudflare Workers.

bun add elm-ssr
Single-app project

Scaffold with init.

Run elm-ssr init in an empty directory. It generates elm-ssr.config.json, runtime.ts, worker.ts, starter routes, and an island.

mkdir my-app && cd my-app
bun add elm-ssr

bunx elm-ssr init my-app
bunx elm-ssr init my-app --db
bunx elm-ssr init my-app --auth betterAuth
Multi-app workspace

Add to an existing workspace.

Use elm-ssr new to add an app to a workspace that already has elm-ssr.config.json. Use --in apps to group under a subdirectory.

bunx elm-ssr new my-app
bunx elm-ssr new my-app --in apps
bunx elm-ssr new my-app --db --auth betterAuth
Project layout

What the scaffold creates.

Routes live in src/<Namespace>/Routes/, islands in Islands/. The CLI scans both on build. Generated output goes to generated/ and is gitignored.

elm-ssr.config.json
package.json
my-app/
  elm.json
  runtime.ts          # createWorkerApp(...)
  worker.ts           # export default worker
  src/MyApp/
    Routes/
      Index.elm
      Counter.elm
      NotFound.elm
    Islands/
      Counter.elm
    View/
      Shared.elm
  migrations/         # optional, with --db
generated/            # build output (gitignored)
Build

Generate and compile.

The build command scans your routes and islands, generates the Elm router and island manifest, syncs the authoring modules, then runs elm make. Run it every time you add or rename a route or island.

bunx elm-ssr build
Local dev — Cloudflare

Run with wrangler dev.

Runs build then wrangler dev. Gives you a local Cloudflare Workers environment with D1, KV, and Queues bindings.

bunx elm-ssr dev
Local dev — plain Bun

Run with Bun.serve.

The generated runtime exports a standard worker.fetch handler. Wire it directly into any Fetch-compatible runtime.

import { worker } from "./my-app/runtime";

Bun.serve({
  port: 3000,
  fetch: (req) => worker.fetch(req, process.env)
});
Deploy

Same handler, any host.

Provider-specific code stays in runtime.ts. The Elm routes and islands are untouched. For Cloudflare Workers, compress before deploying to serve pre-gzipped bundles.

bunx elm-ssr compress
wrangler deploy