Documentation.Find it fast.
Search by keyword or browse by category. All links go to pages on this site.
Getting Started
4Getting Started
Install elm-ssr, scaffold a project with elm-ssr init or new, build with elm-ssr build, run locally with elm-ssr dev or Bun.serve, deploy with wrangler deploy.
Project Layout
elm-ssr.config.json at workspace root. Routes/ and Islands/ under src/<Namespace>/. Generated output in generated/ (gitignored). Optional migrations/ with --db.
CLI Reference
All commands: build, compress, dev, init, new, migrate up/down/status, route (--api --ws --sse), query, routes, info. Global --root flag.
Deployment
Same Fetch-compatible runtime on Bun, Cloudflare Workers, Deno, or any edge host. Plain Bun server, Cloudflare Workers wrangler.jsonc, createWorkerApp, renderApp, worker.fetch adapter shape.
Routes & Requests
4File-Based Routing
Index.elm maps to folder path. Nested modules map to nested URLs. Trailing underscore = dynamic segment (Routes/Posts/Slug_.elm → /posts/:slug). NotFound.elm is the fallback. Route.param reads dynamic segments. page and action exports.
Reading Request Data
Route.method, Route.query, Route.param, Route.formValue, Route.env. Flat JSON bodies decoded with Route.formValue (values must be strings). Route.env for synchronous env access in loaders. Route.path, Route.headers.
Request Decode — type-safe form validation
Decode.decodeForm, decodeQuery, decodeParams, decodeRaw. Pipeline: Decode.succeed |> Decode.required |> Decode.optional |> Decode.optionalWithDefault. Validators: email, nonEmpty, minInt, maxInt, minFloat, maxFloat, minLength, maxLength, validate, custom. Decode.andThen for sequential validation. Accumulates ALL errors before returning Err — PRG error display.
Error Handling
Loader.fail and Action.fail with HTTP status codes. Effect decode failures produce 502. Page.notFound, Page.document, Page.error for custom error pages. Loader.requireUser with malformed session produces 502 (not redirect) — document this footgun. softExecute returns Err ConstraintError. Uncaught exception format. Error format by route prefix: /api/ returns JSON; page routes return HTML.
Data Loading
5Loaders — data fetching pipeline
Loader.succeed, fail, map, map2, andThen, redirect (auth guard shortcut). Loader.requireUser decodes session cookie and redirects unauthenticated requests. Loader.redirect sends a 302 without running an Action. Loader.custom for escape hatch to custom TypeScript effects. Loader.fetchJson, cacheGet, cachePut, env, getCookie, enqueue, session, csrfToken, startJob, jobStatus. page : Request -> Loader (Document msg) contract.
SQL Effects — query, execute, soft writes, transactions
Loader.query (list), Loader.queryOne (Maybe). Loader.execute { sql, params } returns { rowsAffected }. Loader.softExecute and Loader.softQueryOne return Ok / Err ConstraintError instead of failing. Loader.transaction wraps multi-statement writes atomically — rollback on any failure. Requires sqlTransaction configured on adapter.
Cache & HTTP Effects
Loader.fetchJson { url, decoder } — HTTP GET. Loader.cacheGet { key, decoder } returns Maybe (null on miss). Loader.cachePut { key, value, ttlSeconds? }. Backend-neutral: withCache wraps any runner, redisCache(redis) or cloudflareEffects (KV). Pattern: cacheGet → fetchJson on miss → cachePut.
Effects Vocabulary — every effect kind
Flat table of all Loader effect kinds and which adapter handles them: fetchJson, cacheGet, cachePut, query, queryOne, execute, softExecute, softQueryOne, transaction, env, cookie, enqueue, session, csrfToken, setSession, clearSession, startJob, jobStatus, custom. Which effects need sessionMiddleware. Failure shape { ok, error }. Adapter composition order.
Loader.custom — escape hatch for your own effects
Loader.custom { kind, payload, decoder }. Define a custom effect kind handled in TypeScript adapter. Used for fan-out Promise.all, AI / vector / search calls, third-party APIs, service aggregations. Your handler receives payload, returns { ok, value } or { ok: false, error }.
Form Handling
3Actions — non-GET responses
Action.succeed, fail, redirect (303 PRG), json (JSON response), fromLoader (reuse Loader effect), map, andThen, requireUser. action : Request -> Action (Document msg) contract. Actions can redirect, return JSON, return a document, or fail with a status.
Cookies — setting and reading
Action.setCookie, Action.sessionCookie (HttpOnly Secure SameSite=Lax Path=/), Action.defaultCookie (permissive: Path=/ only), Action.clearCookie. Attach cookies to redirects, JSON responses, documents, or failures. Stacking: call setCookie multiple times. Loader.getCookie to read back in page loaders. Local dev: Secure cookies rejected on http:// — use secure: false in sessions config.
PRG Pattern — Post/Redirect/Get
Standard form flow: POST action validates → write → Action.redirect to GET page. Prevents duplicate submissions on browser refresh. Error display via query string (?err_email=required). Used in guestbook, login, signup, and most form routes. Pair with Request.Decode for type-safe validation.
Auth & Sessions
4Sessions & CSRF
Signed-cookie sessions. Loader.session decoder, csrfToken, setSession, clearSession. Stores: memorySessionStore (dev), cacheStore(redisCache(redis)) (prod). Wire in createWorkerApp: sessions { secret, store, secure? } and csrf: true. CSRF on POST/PUT/PATCH/DELETE via X-CSRF-Token header or _csrf form field. Security: secret rotation invalidates all sessions.
Loader.requireUser — protected pages
Loader.requireUser userDecoder request decodes session into your User type and redirects unauthenticated requests to /login (configurable). FOOTGUN: malformed session cookie produces 502 (not redirect) — pre-seed test store with valid payload. Combine with Loader.map2 to load session + CSRF token simultaneously.
Action.requireUser — protected POST actions
Action.requireUser userDecoder request — same as Loader.requireUser but on the Action side. Unauthenticated POST → 302 to login; authenticated POST → action body runs (continues chain). Prevents state mutations without a valid session. Key-rotation test: old-secret cookie rejected by new-secret worker.
Auth Flow Tutorial
Step-by-step: wire sessions + csrf in runtime.ts (memorySessionStore), login page using Loader.map2 (session + csrfToken simultaneously), Loader.setSession on login, Loader.requireUser and Action.requireUser on protected pages, Loader.clearSession for logout. Production checklist.
Islands & Client
3Islands — Browser.element in SSR pages
Island.embed name { encodeFlags, fallback, id } flags. Pages use ElmSsr.Html / ElmSsr.Svg (serializable, no DOM). Islands use elm/html (browser runtime). Stable id preserves island across SPA navigations (transfer, not teardown). Fallback markup renders server-side. elm-ssr-island marker in generated HTML.
Cross-Island Bus — Shared state between islands
ElmSsr.Island.Shared for broadcasting between sibling islands. Shared.send / Shared.broadcast to emit typed messages. Shared.subscribe / Shared.listen to receive in another island. Counter island broadcasts; Observer island receives and updates its view. Cross-island SSE event distribution.
Server-Sent Events — SSE streaming
Server: createSseStream(request, async (send, signal) => loop). createNamedSseStream adds event: name stamp and auto-incrementing id:. Island: Sse.open url (opens EventSource), Sse.events GotEvent (subscription), Sse.match url decoder event (filter + decode), Sse.close, Sse.errors for NetworkError / DecodeError. Auto-reconnect built in. Caveats: use for push updates not initial page data.
Real-time & Background
2Tasks & Queues — background work
Loader.enqueue { task, payload }. withTasks (inline): ctx.waitUntil when available, detached promise locally — good for audits, cache warmups, non-critical notifications. withQueueProducer + createQueueConsumer (Cloudflare Queues): durable, survives request, retried on failure. Choose queues when work MUST survive the request.
Background Jobs — long-running work
Loader.startJob { kind, payload } returns String job id immediately. Loader.jobStatus { jobId, decoder } decodes JobStatus ADT: Queued | Running | Done a | Failed | Missing. withJobs adapter wraps runner. Stores: cacheJobStore. Handler contract: async (payload, ctx) => result. Render polling status page or stream progress with SSE.
Database
3Type-Safe Query DSL
elm-ssr query generates Elm modules from your schema. Phantom types, column descriptors, decoders, camelCase, nullable → Maybe. CRUD builders. Filters: eq, like, between, in_, isNull. Pagination: limit, offset, orderBy.
Elmto — Ecto-like ORM
Schemas, changesets, joins, group-by, aggregates, batch insert, associations, CTEs, subqueries, transactions, bulk updates and deletes. For queries the DSL cannot express.
Migrations
SQL-file migrations applied alphabetically. elm-ssr migrate up / down <count> / status. runMigrations, revertMigrations, listMigrations programmatic API. MigrationsAdapter for bun:sqlite, Postgres via Bun.sql, Cloudflare D1. Each migration + its tracking insert runs transactionally. elm_ssr_migrations tracking table.
Runtime & Backends
3Backends — effect adapter composition
inMemoryEffects({ sql, env, cache }) for tests and local Bun. cloudflareEffects({ cacheBinding, dbBinding }) for D1 + KV. SQL: postgresSql({ run }) wraps any pg-compatible client. Cache: withCache(runner, backend), redisCache(redis). Full composition: withJobs(withTasks(sessionEffects(withCache(inMemoryEffects(...), redisCache(redis))), handlers), jobOptions). Adapter composition order matters: outer intercepts first.
Middleware
Default stack (automatic): errorHandler, requestId, timing, logger, HEAD. Sessions add sessionMiddleware + csrfMiddleware. Custom middleware via composeMiddleware([...fns]). AppContext: { request, env, ctx, requestId, log }. Middleware runs before the Elm route handler.
Worker Setup — createWorkerApp
createWorkerApp({ elmModule, islands, islandsBundle, stylesheet, routes, createFlags, effects?, sessions?, csrf? }). renderApp(elmModule, flags) for programmatic SSR. Route catalog at /api/routes. Built-in endpoints: /health, /api/health, /api/render, /__elm-ssr/islands.js, /__elm-ssr/islands-bundle.js.
Reference
2Parallel SQL Queries — Recipe
Loader.custom + Promise.all in TypeScript adapter for fan-out workloads. Run 3 independent SQL queries in one effect round-trip instead of sequentially. Pattern: { kind: 'parallelDashboard', payload: {}, decoder: dashboardDecoder }. Useful for dashboards and summary pages reading multiple tables.
AI Reference — machine-readable dense docs
docs/ai/ folder: one file per feature, structured as Exports + minimal example + patterns + footguns. Covers routing, loaders-actions, effects-vocabulary (flat table of all effect kinds), backends, tasks, islands, deployment, sessions, SSE, jobs, middleware, migrations, CLI, configuration, query-dsl, elmto, debugger, testing. Use for code generation.