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.Buildermarkup backend replaces the BlazeMarkupMtree for 2–4x faster rendering, with zero-copyrespondHtml/respondSvg. - 🔍 Better Typed SQL: multi-column results generate named
SqlRowtypes for record-dot access, more expressions are inferred non-null, andtypedSqlcan start a temporary PostgreSQL instance at compile time whenDATABASE_URLis 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
initContextintoauthMiddleware, with auth state living in the request vault. - 🔄 Simpler responses: controller
actionnow returnsIO ResponseReceived;render/redirectTosend the response directly, withearlyReturnfor conditional exits. - 🌲 Separate web & worker: dev mode now runs
webandworkeras separate processes (new top-levelWorkerMain.hs). - 🗑️
ihp-logremoved: usefast-loggerdirectly 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.
Before — AutoRoute 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 = webRoutesNew 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
actionnow returnsIO ResponseReceivedinstead ofIO ().render,redirectTo,renderJson, and related response helpers return the WAI response token directly instead of throwingResponseException; useearlyReturn/respondAndExitfor conditional exits.ResponseException,handleNoResponseReturned, andhandleRouterExceptionwere removed from the public error-handling path. (#2205) rendernow renders HTML only. JSON rendering moved to the newJsonViewclass andrenderHtmlOrJson; existing views that implementedjsononViewneed a separateJsonViewinstance. (#2589)- Authentication moved out of
initContextand into WAI middleware.initAuthenticationand the old role helper family were removed; configureAuthMiddleware (authMiddleware @User)inConfig.hsinstead. Auth state now lives in the request vault, andFrameworkConfig.authMiddlewarewas renamed toauthenticationMiddleware. (#2259, #2641) - The
ControllerContexttyped-map API was removed.putContext,fromContext,freeze,unfreeze,FrozenControllerContext, theIHP.Controller.Contextmodule, and theihp-contextpackage are gone; use the WAI request vault for per-request state.ControllerContextis now aRequestalias and is no longer re-exported fromIHP.ViewPrelude. (#2632, #2633, #2711) - QueryBuilder join support was removed:
innerJoin,innerJoinThirdTable,labelResults, and the joined-table filter/order helpers are gone. UsetypedSqlfor joins and custom SQL. (#2599) - The
ihp-logpackage andIHP.Log.*modules were removed. Usefast-loggerdirectly through?context.frameworkConfig.logger,?modelContext.logger, orFrameworkConfig.logger. (#2600) typedSqlis stricter:SELECT *andINSERTwithout explicit column lists are rejected by default, andint8/bigintvalues now map toInt64instead ofInteger. QueryBuilderlimitandoffsetalso useInt64to match PostgreSQL. (#2621, #2655, #2677)- Production app packages no longer include binaries for
Application/Script/*.hs. Scripts are exposed as separatescript-Nameflake outputs and apps. (#2715) - Dev mode now starts separate
webandworkerprocesses. Projects with jobs should move the rootWorkerinstance fromMain.hsinto a new top-levelWorkerMain.hs;new-jobcreates this file for new projects. The oldprocesses.ihpdevenv process name is now split intoprocesses.webandprocesses.worker. (#2672) devenv upno longer opens the ToolServer in a browser by default. The URL is printed to the terminal; setIHP_BROWSERto opt back in. (#2713)- Generated app development defaults now promote incomplete pattern matches to errors, and all IHP packages treat missing Cabal
other-modulesentries as errors. (#2687, #2692) - Authentication sessions now store raw UUID bytes.
parseSessionUUIDaccepts 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, andsqlQueryScalarOrNothingare now deprecated. Prefer the compile-time-checked[typedSql| … |]quasi-quoter withsqlQueryTyped/sqlExecTyped(fromIHP.TypedSql). When you genuinely need untyped/dynamic SQL (dynamic table names, DDL), switch to the newunsafeSql*variants — identical behavior, no warning. (#2651)
New Features
- New explicit routes DSL via
[routes|...|]forWeb/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-routerpackage 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 throughIHP.Router.IHP. (#2657, #2658, #2659, #2660) typedSqlcan start a temporary private PostgreSQL instance during compile-time query description whenDATABASE_URLis unavailable andIHP_TYPED_SQL_AUTO_DB=1is set; IHP dev shells enable this by default. (#2587, #2714)typedSqlmulti-column results now generate namedSqlRowtypes 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
fetchVectorandfetchVectorPipelinedfor returningVectorresults from QueryBuilder queries. (#2592) - Added
buildMailto the publicIHP.Mailexports. (#2586) - Added
uncheckedHsxandcustomHsxto the controller/view preludes, and addedisEmptyforHtml/MarkupM. (#2594, #2619) - Schema parsing and codegen now understand more PostgreSQL syntax:
ANY(ARRAY[...])in checks,NULLS [NOT] DISTINCTindexes, pgvector columns/indexes, VARIADIC function arguments in pg_dump indexes, functionSEToptions, and chained postfix operators such as qualified columns withIN, casts, and field access. (#2614, #2635, #2673, #2681, #2682, #2683) - Added an
IsScalar Integerinstance soBIGSERIAL/BIGINTprimary 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 theihp.ghcCompileroption is now wired through so projects can opt into GHC 9.14 tooling. (#2694, #2705, #2710) - The dev server honors the
PORTenvironment variable. (#2717) - Apache request logs can default
%uto the current user's UUID. (#2649)
Performance, Build, and Tooling
- HSX rendering now uses a direct
ByteString.Buildermarkup backend instead of the BlazeMarkupMtree, giving 2-4x faster HSX rendering in benchmarks.respondHtml/respondSvguse WAI builders directly for zero-copy responses. (#2563) pathToandrenderFieldForUrlare markedNOINLINEto 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
ihplibrary closure is smaller after dropping unusedlens/wreqdependencies and movingIHP.Test.Mockingso normal apps do not pull inhspec. (#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
createManystatements so columns with defaults, includingid, useDEFAULTcorrectly 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/authMiddlewaretests preserving mocked sessions acrosssessionMiddleware. (#2639) - AJAX/fetch requests now get JSON error responses from the error middleware. (#2634)
- Restored
ConvertibleStringsText/StringHtmlinstances for compatibility. (#2638) - Fixed
renderFilteraccumulating 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)