Skip to content

v1.6.0

Latest

Choose a tag to compare

@mpscholten mpscholten released this 21 Jun 10:41
71b7bb6

IHP is a modern batteries-included web framework, built on top of Haskell and Nix. Blazing fast, secure, easy to refactor and the best developer experience with everything you need - from prototype to production.

v1.6.0 introduces an explicit routes DSL and a standalone ihp-router package, 2–4x faster HSX rendering, a stricter and more capable typedSql, a TypeScript DataSync client, and moves authentication and response handling onto WAI middleware.

Highlights

  • 🧭 Explicit Routes DSL: declare URLs with the new [routes|…|] quasi-quoter — path captures, query parameters, WebSocket routes, and proper 400/405 responses. Built on a trie router that's 10–50× faster than the old reflection-based routing. Scaffolded by default for new controllers; AutoRoute still works but is now legacy.
  • 📦 Standalone ihp-router: the trie router, WAI middleware, and [routes|…|] DSL now ship as a package with zero IHP dependencies — usable from plain WAI apps.
  • Faster HSX: a direct ByteString.Builder markup backend replaces the Blaze MarkupM tree for 2–4x faster rendering, with zero-copy respondHtml / respondSvg.
  • 🔍 Better Typed SQL: multi-column results generate named SqlRow types for record-dot access, more expressions are inferred non-null, and typedSql can start a temporary PostgreSQL instance at compile time when DATABASE_URL is unset.
  • 🟦 DataSync in TypeScript: the JavaScript client is rewritten in TypeScript with generated table registries and typed event maps.
  • 🔒 Auth as WAI middleware: authentication moved out of initContext into authMiddleware, with auth state living in the request vault.
  • 🔄 Simpler responses: controller action now returns IO ResponseReceived; render / redirectTo send the response directly, with earlyReturn for conditional exits.
  • 🌲 Separate web & worker: dev mode now runs web and worker as separate processes (new top-level WorkerMain.hs).
  • 🗑️ ihp-log removed: use fast-logger directly via ?context.frameworkConfig.logger.

⬆️ Upgrading? See the Upgrade Guide for step-by-step migration instructions.


Major Changes

Explicit Routes DSL

The new [routes|…|] quasi-quoter makes the URL ↔ action mapping explicit in Web/Routes.hs. The action record's field types drive how path captures ({name}, RFC 6570) and query parameters (?name) are parsed.

BeforeAutoRoute derives every URL from the action constructor names:

-- Web/Routes.hs
instance AutoRoute PostsController

-- Web/FrontController.hs
controllers = [ parseRoute @PostsController ]

After — each URL is declared explicitly:

-- Web/Routes.hs
[routes|webRoutes
GET    /Posts             PostsAction
GET    /NewPost           NewPostAction
GET    /ShowPost?postId   ShowPostAction
POST   /CreatePost        CreatePostAction
|]

-- Web/FrontController.hs
controllers = webRoutes

New controllers are scaffolded with the DSL by default. AutoRoute still works — both compile into the same route trie, so they can coexist in one app — but it is now considered legacy and is planned for removal in a future release; prefer the DSL for new and existing controllers.

Why it's faster: the DSL is built on a new trie-based router that replaces IHP's previous Data-reflection-based routing. Routes are reified at compile time (no runtime reflection, no deriving Data) and compiled into a single trie that is built once at startup, so dispatch is an O(path-depth) trie walk with no backtracking. On IHP's internal ~75-route forum benchmark, routing is 10–50× faster than the old reflection-based router. The trie router and DSL also ship as the standalone ihp-router package with zero IHP dependencies.

Controller actions return ResponseReceived

Controller actions now return IO ResponseReceived instead of IO (). Response helpers (render, redirectTo, renderJson, …) send the WAI response directly and return the response token. Conditional responses use earlyReturn, since when / unless still expect IO ():

action CreatePostAction = do
    post <- fill @Post
    when (not (isValid post)) do
        earlyReturn $ redirectTo NewPostAction
    post <- createRecord post
    redirectTo ShowPostAction { postId = post.id }

Authentication via WAI middleware

Authentication runs as a WAI middleware before your controllers. Remove initAuthentication @User from your InitControllerContext instance and add the middleware in Config.hs:

-- Config.hs
option $ AuthMiddleware (authMiddleware @User)

The current user still comes from currentUserOrNothing / currentUser; the removed role helpers (currentRole, ensureIsRole, …) are replaced by type-specific variants such as currentUser / ensureIsUser.

Stricter, smarter Typed SQL

SELECT * is convenient — and AI coding agents reach for it constantly — but it binds result columns by position. When the schema drifts between development and production (a column added, dropped, or reordered, or migrations applied in a different order), those positions no longer line up: the decoder silently maps values to the wrong fields. That's a whole class of subtle, hard-to-trace data bugs.

To prevent it, typedSql now rejects SELECT * and INSERT without explicit column lists by default — naming every column keeps a query correct regardless of physical column order. On top of that: int8 / bigint now map to Int64, multi-column results generate named SqlRow types for record-dot field access, and more expressions (COUNT, EXISTS, window functions, non-null literals) are inferred as non-null. When DATABASE_URL is unavailable, set IHP_TYPED_SQL_AUTO_DB=1 to let typedSql start a temporary private PostgreSQL instance at compile time — IHP dev shells enable this by default.


Full Changelog

Breaking Changes

  • Controller action now returns IO ResponseReceived instead of IO (). render, redirectTo, renderJson, and related response helpers return the WAI response token directly instead of throwing ResponseException; use earlyReturn / respondAndExit for conditional exits. ResponseException, handleNoResponseReturned, and handleRouterException were removed from the public error-handling path. (#2205)
  • render now renders HTML only. JSON rendering moved to the new JsonView class and renderHtmlOrJson; existing views that implemented json on View need a separate JsonView instance. (#2589)
  • Authentication moved out of initContext and into WAI middleware. initAuthentication and the old role helper family were removed; configure AuthMiddleware (authMiddleware @User) in Config.hs instead. Auth state now lives in the request vault, and FrameworkConfig.authMiddleware was renamed to authenticationMiddleware. (#2259, #2641)
  • The ControllerContext typed-map API was removed. putContext, fromContext, freeze, unfreeze, FrozenControllerContext, the IHP.Controller.Context module, and the ihp-context package are gone; use the WAI request vault for per-request state. ControllerContext is now a Request alias and is no longer re-exported from IHP.ViewPrelude. (#2632, #2633, #2711)
  • QueryBuilder join support was removed: innerJoin, innerJoinThirdTable, labelResults, and the joined-table filter/order helpers are gone. Use typedSql for joins and custom SQL. (#2599)
  • The ihp-log package and IHP.Log.* modules were removed. Use fast-logger directly through ?context.frameworkConfig.logger, ?modelContext.logger, or FrameworkConfig.logger. (#2600)
  • typedSql is stricter: SELECT * and INSERT without explicit column lists are rejected by default, and int8 / bigint values now map to Int64 instead of Integer. QueryBuilder limit and offset also use Int64 to match PostgreSQL. (#2621, #2655, #2677)
  • Production app packages no longer include binaries for Application/Script/*.hs. Scripts are exposed as separate script-Name flake outputs and apps. (#2715)
  • Dev mode now starts separate web and worker processes. Projects with jobs should move the root Worker instance from Main.hs into a new top-level WorkerMain.hs; new-job creates this file for new projects. The old processes.ihp devenv process name is now split into processes.web and processes.worker. (#2672)
  • devenv up no longer opens the ToolServer in a browser by default. The URL is printed to the terminal; set IHP_BROWSER to opt back in. (#2713)
  • Generated app development defaults now promote incomplete pattern matches to errors, and all IHP packages treat missing Cabal other-modules entries as errors. (#2687, #2692)
  • Authentication sessions now store raw UUID bytes. parseSessionUUID accepts both the current raw UUID format and legacy 44-byte cereal-encoded UUID values for compatibility, but custom login/session code should write the raw format. (#2640, #2680)

Deprecations

  • The untyped raw-SQL helpers sqlQuery, sqlQuerySingleRow, sqlExec, sqlExecDiscardResult, sqlQueryScalar, and sqlQueryScalarOrNothing are now deprecated. Prefer the compile-time-checked [typedSql| … |] quasi-quoter with sqlQueryTyped / sqlExecTyped (from IHP.TypedSql). When you genuinely need untyped/dynamic SQL (dynamic table names, DDL), switch to the new unsafeSql* variants — identical behavior, no warning. (#2651)

New Features

  • New explicit routes DSL via [routes|...|] for Web/Routes.hs, including RFC 6570 path captures, explicit query parameters, multi-controller route blocks, WebSocket routes, GET-to-HEAD handling, and 400/405 responses for invalid methods. New controllers are scaffolded with the DSL by default while AutoRoute remains supported. (#2652, #2663, #2678, #2679)
  • New standalone ihp-router package containing the trie router, WAI middleware, URL capture machinery, and [routes|...|] quasi-quoter without depending on the rest of IHP. IHP re-exports the IHP-specific router integration through IHP.Router.IHP. (#2657, #2658, #2659, #2660)
  • typedSql can start a temporary private PostgreSQL instance during compile-time query description when DATABASE_URL is unavailable and IHP_TYPED_SQL_AUTO_DB=1 is set; IHP dev shells enable this by default. (#2587, #2714)
  • typedSql multi-column results now generate named SqlRow types for record-dot field access, infer more non-null expressions (COUNT, EXISTS, window functions, non-null literals), and report polymorphic placeholder type errors with better messages. (#2591, #2596, #2669)
  • Added fetchVector and fetchVectorPipelined for returning Vector results from QueryBuilder queries. (#2592)
  • Added buildMail to the public IHP.Mail exports. (#2586)
  • Added uncheckedHsx and customHsx to the controller/view preludes, and added isEmpty for Html / MarkupM. (#2594, #2619)
  • Schema parsing and codegen now understand more PostgreSQL syntax: ANY(ARRAY[...]) in checks, NULLS [NOT] DISTINCT indexes, pgvector columns/indexes, VARIADIC function arguments in pg_dump indexes, function SET options, and chained postfix operators such as qualified columns with IN, casts, and field access. (#2614, #2635, #2673, #2681, #2682, #2683)
  • Added an IsScalar Integer instance so BIGSERIAL / BIGINT primary keys compile, and pooled PostgreSQL connections now pin the session timezone to UTC. (#2647, #2648)
  • DataSync's JavaScript client is now authored in TypeScript with generated table registries, typed event maps, and narrower generic public APIs. (#2578)
  • IHP projects can put local Haskell libraries under lib, and the ihp.ghcCompiler option is now wired through so projects can opt into GHC 9.14 tooling. (#2694, #2705, #2710)
  • The dev server honors the PORT environment variable. (#2717)
  • Apache request logs can default %u to the current user's UUID. (#2649)

Performance, Build, and Tooling

  • HSX rendering now uses a direct ByteString.Builder markup backend instead of the Blaze MarkupM tree, giving 2-4x faster HSX rendering in benchmarks. respondHtml / respondSvg use WAI builders directly for zero-copy responses. (#2563)
  • pathTo and renderFieldForUrl are marked NOINLINE to reduce Core bloat, and the new routes DSL caches the merged route trie at application construction time. (#2524, #2652)
  • Nix production builds now compile web, worker, and script executables in separate derivations, allowing more parallelism and better cache reuse. (#2715)
  • Nix evaluation no longer relies on eval-time import-from-derivation for IHP data paths or Hackage package metadata; Hackage overrides are pre-generated. (#2612, #2620)
  • The core ihp library closure is smaller after dropping unused lens / wreq dependencies and moving IHP.Test.Mocking so normal apps do not pull in hspec. (#2626, #2627)
  • Dev-server GHCi now uses the copying GC so memory is returned to the OS more aggressively, and job-worker shutdown drains in-flight work more reliably. (#2712, #2721)
  • CI now uses Magic Nix Cache and the self-hosted digitally induced binary cache, and GitHub-hosted runners avoid multi-GHC OOM failures. (#2605, #2608, #2609, #2700, #2703, #2704)
  • The Guide layout was redesigned and the Guide/API-doc links were cleaned up, including generated API documentation links for extracted subpackages. (#2666, #2697, #2716)

Bug Fixes

  • Fixed generated createMany statements so columns with defaults, including id, use DEFAULT correctly and mixed touched-field sets are handled per row. (#2585)
  • Fixed DataSync concurrent trigger installation by using the correct advisory lock key. (#2595)
  • Fixed filterWhere / generated field-name handling for keyword-escaped fields ending in an underscore. (#2602)
  • Fixed WebSocket upgrades being logged as 500 instead of 101, fixed AutoRefresh WebSocket fallback behavior, and restored the AutoRefresh meta tag after the request-vault migration. (#2625, #2631, #2637)
  • Fixed withUser / authMiddleware tests preserving mocked sessions across sessionMiddleware. (#2639)
  • AJAX/fetch requests now get JSON error responses from the error middleware. (#2634)
  • Restored ConvertibleStrings Text/String Html instances for compatibility. (#2638)
  • Fixed renderFilter accumulating duplicate query parameters. (#2719)
  • Fixed router exception wrapping so router exceptions are handled by the framework error path. (#2718)
  • Missing prepared statements are retried after PostgreSQL invalidates a prepared statement cache entry. (#2724)
  • Migration tooling now shows actual migration failure messages and fails on duplicate migration timestamps. (#2617, #2723)
  • Fixed orphaned GHCi processes on Ctrl+C / SIGTERM, and surfaced app startup crashes on the dev-server error page. (#2603, #2607, #2722)