Config

At a certain point in the lifetime of your IHP app you will want to add your own config parameters, e.g. for managing secrets, API keys or external services. This guide explains the best practises for doing that.

Custom Configuration

Dealing with Secrets

Sometimes you want to have a custom configuration flag inside your application.

The recommended way is to declare a custom newtype in Config/Config.hs like this:

-- Config.hs

import IHP.EnvVar

newtype StripePublicKey = StripePublicKey Text

We want our new config parameter to be filled from a STRIPE_PUBLIC_KEY env variable. Therefore we add this to our Config.hs:

module Config where

import IHP.EnvVar

newtype StripePublicKey = StripePublicKey Text

config :: ConfigBuilder
config = do
    -- ...
    stripePublicKey <- StripePublicKey <$> env @Text "STRIPE_PUBLIC_KEY"
    option stripePublicKey

Now the app reads the STRIPE_PUBLIC_KEY env variable at startup and makes it available to the app.

Before we proceed we should add a default value for this in dev mode. Create a file .env and add the following env variables:

# Add this at the end of the file
export STRIPE_PUBLIC_KEY="pk_test_..."

The .env is not committed to the repo as it’s part of the default .gitignore for IHP projects. The .envrc has a snippet to load environment variables from .env into your shell.

If you are ok to commit your secrets to git repo, you can also put the env vars directly into the .envrc file.

Using Custom Config Parameters

You can now access the StripePublicKey parameter by calling getAppConfig @Config.StripePublicKey:

action MyAction = do
    let (StripePublicKey stripePublicKey) = getAppConfig @Config.StripePublicKey

    putStrLn ("Stripe public key: " <> stripePublicKey)

If you want to fetch it in a helper function, we need to define the ?context:

getStripePublicKey :: (?context :: ControllerContext) => StripePublicKey
getStripePublicKey = getAppConfig @Config.StripePublicKey

Environment Variables

Reading Environment Variables

Inside Config/Config.hs you can use env to read environment variables.

module Config where

config :: ConfigBuilder
config = do
    someString <- env @Text "SOME_STRING"

The env function will raise an error if the env var is not defined.

The env function can also deal with other common types:

module Config where

config :: ConfigBuilder
config = do
    maxRetryCount <- env @Int "MAX_RETRY_COUNT"
    byteString <- env @ByteString "SOME_BYTESTRING"

Default Values

Use envOrDefault to provide a default value for an env var:

module Config where

config :: ConfigBuilder
config = do
    redisPort <- envOrDefault @Int 6379 "REDIS_PORT"

Optional Env Variables

When an env variable is optional and has no good default value, use envOrNothing. It will return Nothing if the env variable is not set:

module Config where

config :: ConfigBuilder
config = do
    redisUrl :: Maybe Text <- envOrNothing "REDIS_URL"

Custom Parser

When you’re dealing with a custom enum type it can be useful to write a custom env parser by implementing an EnvVarReader:

module Config where

config :: ConfigBuilder
config = do
    ipAddrSource :: IPAddrSource <- envOrDefault "IP_ADDR_SOURCE" FromSocket

data IPAddrSource = FromSocket | FromHeader

instance EnvVarReader RequestLogger.IPAddrSource where
    envStringToValue "FromHeader" = Right RequestLogger.FromHeader
    envStringToValue "FromSocket" = Right RequestLogger.FromSocket
    envStringToValue otherwise    = Left "Expected 'FromHeader' or 'FromSocket'"

Custom Middleware

IHP provides an “escape-hatch” from the framework with the CustomMiddleware option. This can be used to run any WAI middleware after IHP’s middleware stack, allowing for possibilities such as embedding a Servant or Yesod app into an IHP app, adding GZIP compression, or any other number of possibilities. See wai-extra for examples of WAI middleware that could be added.

The following example sets up a custom middleware that infers the real IP using X-Forwarded-For and adds a custom header for every request.

module Config where

import Network.Wai.Middleware.AddHeaders (addHeaders)
import Network.Wai.Middleware.RealIp (realIp)

config :: ConfigBuilder
config = do
    option $ CustomMiddleware $ addHeaders [("X-My-Header", "Custom WAI Middleware!")] . realIp

Compression Middleware

We can compress assets using gzip or brotli. First, let’s add the required Haskell dependencies: In your default.nix file, add:

        haskellDeps = p: with p; [
            ...

            # Wai Middleware
            wai-middleware-brotli # <-- Add This Dependency
            wai-extra # <-- And This One
        ];

Run devenv up to update the environment. Once that succeeds, we can use it in your Config/Config.hs:

Add two imports, one for Gzip compression, another for Brotli compression:

module Config where
...
import Network.Wai.Middleware.Brotli -- <-- Add This Import
import Network.Wai.Middleware.Gzip -- <-- And This One

And then create a function compressionMiddleware that combines (composes) Gzip and Brotli compression middleware’s into one middleware:

-- | Gzip And Brotli Compression Middleware
compressionMiddleware :: CustomMiddleware
compressionMiddleware =
    let
        -- With `GzipCompress` and `BrotliCompress` options, it will compress per request.
        gzipSettings = def { gzipFiles = GzipCompress }
        brotliSettings = defaultSettings { brotliFilesBehavior = BrotliCompress }
    in
        CustomMiddleware (gzip gzipSettings . brotli brotliSettings)

Lastly, we can use it as:

config :: ConfigBuilder
config = do
    ...
    option compressionMiddleware -- <-- Here we add our middleware

The default behavior for GzipCompress and BrotliCompress is to compress files on the fly. You can customize this behavior, take a look at the brotli config and gzip config.

Also notice CustomMiddleware (gzip gzipSettings . brotli brotliSettings), It’s important that brotli middleware wraps the gzip middleware, so the responses are not compressed by both, if the client supports brotli, compress with brotli, otherwise gzip, fallback to no compression.

By default all text/* content types will be compressed, including application/json, application/javascript, application/ecmascript and image/x-icon. Simply put, html, text, css, javascript, json and icons.

Database Connection Pool

IHP uses two database connection pools:

  1. postgresql-simple pool - Used for inserts, updates, deletes, and transactions
  2. hasql pool - Used for fetch queries with prepared statements (better performance)

Hasql Pool Configuration

The hasql pool can be configured using environment variables:

VariableDefaultDescription
HASQL_POOL_SIZE20Number of connections in the pool
HASQL_IDLE_TIME600Seconds before idle connections are closed

Example .env configuration:

# Use a single connection for consistent prepared statement caching
export HASQL_POOL_SIZE=1

# Keep connections alive for 30 minutes
export HASQL_IDLE_TIME=1800

Prepared Statement Caching

PostgreSQL prepared statements are cached per-connection. With multiple connections in the pool, the first query on each connection will re-prepare the statement.

Configuration Reference

This section provides a comprehensive reference of all configuration options available in IHP. Configuration is set in your Config/Config.hs file using the option function, or through environment variables.

How Configuration Works

IHP uses a type-based configuration system. Each configuration option is a distinct Haskell type, and you set options using the option function inside Config/Config.hs:

module Config where

import IHP.Prelude
import IHP.Environment
import IHP.FrameworkConfig

config :: ConfigBuilder
config = do
    option Production
    option (AppHostname "myapp.com")

The first call to option for a given type wins. If your Config.hs sets a value and the IHP defaults also set a value for the same type, your value takes priority because config runs before ihpDefaultConfig.

Server Settings

Environment

Controls whether the app runs in Development or Production mode. Many other settings change their defaults based on this value (logging, caching, error pages, etc.).

TypeEnvironment
ValuesDevelopment, Production
DefaultDevelopment
Env varIHP_ENV (set to "Production" or "Development")
-- Config.hs
config = do
    option Production

The key differences between environments are:

App Port

The port the HTTP server listens on.

TypeAppPort
Default8000
Env varPORT
-- Config.hs
config = do
    option (AppPort 3000)

App Hostname

The hostname used when constructing the base URL.

TypeAppHostname
Default"localhost"
-- Config.hs
config = do
    option (AppHostname "myapp.com")

Base URL

The full base URL of the application (e.g. "https://myapp.com"). This is normally constructed automatically from AppHostname and AppPort, but can be overridden.

TypeBaseUrl
DefaultBuilt from hostname and port, e.g. "http://localhost:8000"
Env varIHP_BASEURL (overrides the computed value)
-- Config.hs
config = do
    option (BaseUrl "https://myapp.com")

The IHP_BASEURL environment variable is particularly useful in production deployments where the app runs behind a reverse proxy.

Database

Database URL

The PostgreSQL connection string.

TypeDatabaseUrl
Default"postgresql:///app?host=<project-dir>/build/db" (local Unix socket)
Env varDATABASE_URL
-- Config.hs
config = do
    option (DatabaseUrl "postgresql://user:pass@host:5432/dbname")

In production, set the DATABASE_URL environment variable instead of hardcoding credentials in source code.

Hasql Connection Pool

The hasql pool is used for fetch queries with prepared statements. Configure it via environment variables:

Env varDefaultDescription
HASQL_POOL_SIZE20Number of connections in the pool
HASQL_IDLE_TIMENot set (uses hasql default)Seconds before idle connections are closed
# .env
export HASQL_POOL_SIZE=1
export HASQL_IDLE_TIME=1800

Setting HASQL_POOL_SIZE=1 gives consistent prepared statement caching since PostgreSQL caches prepared statements per-connection.

Session

Controls the session cookie behavior (max age, security flags, same-site policy).

TypeSessionCookie
DefaultPath: /, Max-Age: 30 days, SameSite: Lax, HttpOnly: yes, Secure: yes if base URL uses HTTPS
-- Config.hs
import qualified Web.Cookie as Cookie

config = do
    option $ SessionCookie (defaultIHPSessionCookie "https://myapp.com")
        { Cookie.setCookieMaxAge = Just (fromIntegral (60 * 60 * 24 * 90)) -- 90 days
        , Cookie.setCookieSameSite = Just Cookie.sameSiteStrict
        }

Session Secret

The session encryption key. IHP looks for this in the following order:

  1. IHP_SESSION_SECRET_FILE env var – path to a key file
  2. IHP_SESSION_SECRET env var – the key value directly
  3. Config/client_session_key.aes file (auto-generated in development)
Env varDescription
IHP_SESSION_SECRET_FILEPath to a file containing the session encryption key
IHP_SESSION_SECRETThe session encryption key as a string

In production, set one of these environment variables. In development, IHP auto-generates and uses the Config/client_session_key.aes file.

Logging

Logger

Controls log level, format, and destination.

TypeLogger
Default (Development)Debug level, default format, stdout
Default (Production)Info level, default format, stdout
-- Config.hs
import IHP.Log as Log
import IHP.Log.Types

config = do
    -- Log only warnings and above
    logger <- liftIO $ newLogger def { level = Warn }
    option logger
Log Levels

Log levels from lowest to highest: Debug, Info, Warn, Error, Fatal, Unknown. Messages below the configured level are discarded.

LevelDescription
DebugGeneral debugging messages, SQL queries. Default in Development.
InfoInformational messages for monitoring. Default in Production.
WarnPotential problems.
ErrorRecoverable application errors.
FatalUnrecoverable errors (does not exit the program).
UnknownAlways logged regardless of level setting.
Log Destinations
-- Log to a file without rotation
logger <- liftIO $ newLogger def { destination = File "Log/production.log" NoRotate defaultBufSize }

-- Log to a file with size-based rotation (4 MB, keep 7 rotated files)
logger <- liftIO $ newLogger def { destination = File "Log/production.log" (SizeRotate (Bytes (4 * 1024 * 1024)) 7) defaultBufSize }

-- Log to stderr
logger <- liftIO $ newLogger def { destination = Stderr defaultBufSize }

-- Disable logging
logger <- liftIO $ newLogger def { destination = None }
Log Formatters
-- Include timestamps
logger <- liftIO $ newLogger def { formatter = withTimeFormatter }

-- Include log level
logger <- liftIO $ newLogger def { formatter = withLevelFormatter }

-- Include both timestamp and log level
logger <- liftIO $ newLogger def { formatter = withTimeAndLevelFormatter }

Request Logger IP Source

Controls how the request logger determines the client IP address.

TypeRequestLogger.IPAddrSource
DefaultFromSocket
Env varIHP_REQUEST_LOGGER_IP_ADDR_SOURCE (set to "FromHeader" or "FromSocket")

Set to FromHeader when running behind a reverse proxy that sets X-Forwarded-For:

-- Config.hs
import qualified Network.Wai.Middleware.RequestLogger as RequestLogger

config = do
    option RequestLogger.FromHeader

Security

CORS

Cross-Origin Resource Sharing policy. Disabled (no CORS headers) by default.

TypeMaybe Cors.CorsResourcePolicy
DefaultNothing (CORS middleware not applied)
-- Config.hs
import qualified Network.Wai.Middleware.Cors as Cors

config = do
    option $ Just Cors.simpleCorsResourcePolicy
        { Cors.corsOrigins = Just (["https://frontend.example.com"], True)
        , Cors.corsMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
        , Cors.corsRequestHeaders = ["Content-Type", "Authorization"]
        }

Request Body Limits

Controls the maximum size of request bodies, uploaded files, number of headers, etc.

TypeWaiParse.ParseRequestBodyOptions
DefaultWaiParse.defaultParseRequestBodyOptions (from wai-extra)
-- Config.hs
import qualified Network.Wai.Parse as WaiParse

config = do
    option $ WaiParse.setMaxRequestFileSize (50 * 1024 * 1024) -- 50 MB
           $ WaiParse.defaultParseRequestBodyOptions

CSS Framework

Controls which CSS framework is used for rendering forms, pagination, flash messages, and other UI components.

TypeCSSFramework
Defaultbootstrap (Bootstrap)
-- Config.hs
import IHP.View.CSSFramework.Bootstrap (bootstrap)

config = do
    option bootstrap

You can customize the CSS framework by overriding individual rendering functions. See the IHP Guide on CSS Frameworks for details.

File Storage

File storage is configured using helper functions from IHP.FileStorage.Config. You must choose one storage backend.

Static Directory Storage

Stores uploaded files in the local static/ directory.

Env varIHP_STORAGE_DIR (default: "static/")
-- Config.hs
import IHP.FileStorage.Config

config = do
    initStaticDirStorage

Amazon S3 Storage

Stores files in an AWS S3 bucket.

Env varDescription
AWS_ACCESS_KEY_IDAWS access key
AWS_SECRET_ACCESS_KEYAWS secret key
-- Config.hs
import IHP.FileStorage.Config

config = do
    initS3Storage "eu-central-1" "my-bucket-name"

Minio Storage

Stores files in a Minio-compatible object storage server.

Env varDescription
MINIO_ACCESS_KEYMinio access key
MINIO_SECRET_KEYMinio secret key
-- Config.hs
import IHP.FileStorage.Config

config = do
    initMinioStorage "https://minio.example.com" "my-bucket-name"

Filebase Storage

Stores files using the Filebase IPFS-backed storage service.

Env varDescription
FILEBASE_KEYFilebase access key
FILEBASE_SECRETFilebase secret key
-- Config.hs
import IHP.FileStorage.Config

config = do
    initFilebaseStorage "my-bucket-name"

Mail

Mail server configuration is set using option with a MailServer value. You must configure a mail server before calling sendMail.

Sendmail (Local)

Uses the local sendmail binary:

-- Config.hs
import IHP.Mail.Types

config = do
    option Sendmail

SMTP

Uses a generic SMTP server:

-- Config.hs
import IHP.Mail.Types

config = do
    option SMTP
        { host = "smtp.example.com"
        , port = 587
        , credentials = Just ("username", "password")
        , encryption = STARTTLS -- or TLS, or Unencrypted
        }

The SMTPEncryption type supports three values: Unencrypted, TLS, and STARTTLS. It can be read from environment variables using env.

Amazon SES

Uses AWS Simple Email Service:

-- Config.hs
import IHP.Mail.Types

config = do
    option SES
        { accessKey = "your-access-key"
        , secretKey = "your-secret-key"
        , region = "us-east-1"
        }

SendGrid

Uses SendGrid for email delivery:

-- Config.hs
import IHP.Mail.Types

config = do
    option SendGrid
        { apiKey = "your-sendgrid-api-key"
        , category = Nothing -- or Just "transactional"
        }

Exception Tracking

Controls how unhandled exceptions are reported. Useful for integrating with services like Sentry.

TypeExceptionTracker
DefaultWarp’s default exception handler
-- Config.hs
config = do
    option $ ExceptionTracker \maybeRequest exception -> do
        putStrLn ("Exception: " <> show exception)
        -- Send to Sentry, Bugsnag, etc.

Asset Versioning

Controls cache-busting for static assets. In production, assetPath appends a version hash to file URLs.

Env varDescription
IHP_ASSET_VERSIONA version string appended to asset URLs for cache busting
IHP_ASSET_BASEURLBase URL prepended to asset paths (e.g. a CDN URL)

These are typically set in your deployment configuration, not in Config.hs.

DataSync (Real-Time)

Settings for IHP DataSync WebSocket connections.

Env varIHP_DATASYNC_MAX_SUBSCRIPTIONS_PER_CONNECTION
Default128
DescriptionMaximum number of DataSync subscriptions per WebSocket connection
Env varIHP_DATASYNC_MAX_TRANSACTIONS_PER_CONNECTION
Default10
DescriptionMaximum number of concurrent DataSync transactions per WebSocket connection
-- Config.hs
config = do
    option (DataSyncMaxSubscriptionsPerConnection 256)
    option (DataSyncMaxTransactionsPerConnection 20)

Row-Level Security

Controls the PostgreSQL role used for queries with Row Level Security enabled.

TypeRLSAuthenticatedRole
Default"ihp_authenticated"
Env varIHP_RLS_AUTHENTICATED_ROLE
-- Config.hs
config = do
    option (RLSAuthenticatedRole "my_app_user")

IDE Integration

The base URL of the IHP IDE (development server UI). Only used in Development mode.

TypeIdeBaseUrl
Default"http://localhost:<port+1>"
Env varIHP_IDE_BASEURL

Startup Initializers

Run custom IO actions when the app server starts. Initializers run concurrently using async.

-- Config.hs
config = do
    addInitializer do
        putStrLn "App server started!"
        -- Warm caches, start background tasks, etc.

Other Environment Variables

These environment variables are read by IHP’s server infrastructure and cannot be set via option in Config.hs:

Env varDefaultDescription
IHP_SYSTEMDFalseEnable systemd socket activation and watchdog support
IHP_STATICIHP’s built-in static directoryOverride the path to IHP’s framework static files
APP_STATIC"static/"Override the path to the application’s static files directory
IHP_SOCKET_FDNot setFile descriptor for a pre-opened socket (used by the dev server for seamless restarts)

Quick Reference: All Environment Variables

VariableDefaultCategory
IHP_ENVDevelopmentServer
PORT8000Server
IHP_BASEURLAuto-computedServer
DATABASE_URLLocal Unix socketDatabase
HASQL_POOL_SIZE20Database
HASQL_IDLE_TIMEhasql defaultDatabase
IHP_SESSION_SECRETN/ASession
IHP_SESSION_SECRET_FILEN/ASession
IHP_REQUEST_LOGGER_IP_ADDR_SOURCEFromSocketLogging
IHP_ASSET_VERSIONN/AAssets
IHP_ASSET_BASEURLN/AAssets
IHP_RLS_AUTHENTICATED_ROLEihp_authenticatedSecurity
IHP_DATASYNC_MAX_SUBSCRIPTIONS_PER_CONNECTION128DataSync
IHP_DATASYNC_MAX_TRANSACTIONS_PER_CONNECTION10DataSync
IHP_SYSTEMDFalseDeployment
IHP_STATICBuilt-inStatic Files
APP_STATICstatic/Static Files
IHP_STORAGE_DIRstatic/File Storage
AWS_ACCESS_KEY_IDN/AFile Storage (S3)
AWS_SECRET_ACCESS_KEYN/AFile Storage (S3)
MINIO_ACCESS_KEYN/AFile Storage (Minio)
MINIO_SECRET_KEYN/AFile Storage (Minio)
FILEBASE_KEYN/AFile Storage (Filebase)
FILEBASE_SECRETN/AFile Storage (Filebase)
IHP_IDE_BASEURLhttp://localhost:<port+1>IDE