### Set Up Local Documentation Development
Source: https://github.com/digitallyinduced/ihp/blob/master/CONTRIBUTING.md
Navigate to the `IHP` directory and run `direnv allow`. Then, navigate to the `Guide` directory and run `direnv allow` again to install dependencies.
```bash
cd IHP
direnv allow
cd Guide
direnv allow
```
--------------------------------
### Install NPM Modules
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/npm.markdown
Use 'npm install' to add required Node modules to your project. This example adds 'tailwindcss', 'postcss', and 'autoprefixer'.
```bash
npm add tailwindcss postcss autoprefixer
```
--------------------------------
### Start the IHP application
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
After building the application with Nix, run this command to start the production server.
```bash
result/bin/RunProdServer
```
--------------------------------
### Setup Integration Test Module (Test/Integration.hs)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/testing.markdown
Configure the main module for integration tests. This setup uses `withIHPApp` and requires a PostgreSQL database with the application schema loaded.
```haskell
-- Test/Integration.hs
module Main where
import Test.Hspec
import IHP.Prelude
import Test.Controller.PostsSpec
main :: IO ()
hspec do
Test.Controller.PostsSpec.tests
```
--------------------------------
### Start Application Service
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Manually start the IHP application service using systemctl.
```bash
systemctl start app.service
```
--------------------------------
### Controller Lifecycle Example
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown
Demonstrates the controller lifecycle where `beforeAction` is executed before the `action`. This example logs 'A' before 'B' when `ShowPostAction` is called.
```haskell
instance Controller PostsController where
beforeAction = putStrLn "A"
action ShowPotsAction { postId } = putStrLn "B"
```
--------------------------------
### Install Nix on Server
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Installs the Nix package manager on your server using the official installation script. This is a prerequisite for building IHP applications.
```bash
curl -L https://nixos.org/nix/install | sh
```
--------------------------------
### Add Startup Initializer
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown
Define custom IO actions to run when the application server starts. These initializers run concurrently using `async` and can be used for tasks like warming caches or starting background processes.
```haskell
-- Config.hs
config = do
addInitializer do
putStrLn "App server started!"
-- Warm caches, start background tasks, etc.
```
--------------------------------
### POST /api/passkey/authenticate/begin
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Starts the passkey authentication process by generating a challenge and returning it to the client.
```APIDOC
## POST /api/passkey/authenticate/begin
### Description
Generates a challenge for passkey authentication and returns the necessary options to the client.
### Method
POST
### Endpoint
/api/passkey/authenticate/begin
### Response
#### Success Response (200)
- **credentialOptions** (object) - Options for the client to initiate credential authentication.
#### Response Example
```json
{
"credentialOptions": {
"challenge": "...",
"rpId": "localhost",
"userVerification": "required"
}
}
```
#### Error Response (400, 422)
- **error** (string) - An error message describing the issue.
```
--------------------------------
### Install IHP CLI and Create New Project
Source: https://github.com/digitallyinduced/ihp/blob/master/README.md
These bash commands show how to install the IHP command-line interface using Nix and then create a new IHP project named 'myproject'. It also includes the command to set up the development environment.
```bash
nix profile install nixpkgs#ihp-new
ihp-new myproject
cd myproject && devenv up
```
--------------------------------
### Install React and IHP JS Helpers
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown
Install React, ReactDOM, esbuild for bundling, and IHP's JavaScript data synchronization helpers using `npm add`. Ensure you are in the `Frontend` directory when running these commands.
```bash
cd Frontend # npm needs to be run from the Frontend directory
# Install react and react-dom
npm add react react-dom
# We also need esbuild for bundling
npm add esbuild
# Install IHP JS helpers
npm add "https://gitpkg.now.sh/digitallyinduced/ihp/lib/IHP/DataSync?0babfec7a90675ca37d56c38dac69e784e64fc83"
```
--------------------------------
### Install IHP using Nix
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown
Use this command to install the IHP framework via the Nix package manager. Ensure Nix is installed and configured.
```bash
nix profile install nixpkgs#ihp-new
```
--------------------------------
### Basic IHP Configuration Example
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown
Demonstrates setting basic configuration options like Production environment and AppHostname in Config.hs.
```haskell
module Config where
import IHP.Prelude
import IHP.Environment
import IHP.FrameworkConfig
config :: ConfigBuilder
config = do
option Production
option (AppHostname "myapp.com")
```
--------------------------------
### Install ihp-new with Nix
Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md
Install the updated `ihp-new` tool using Nix. Choose the appropriate command based on whether you use `nix-env` or `nix profile`.
```bash
# Run this:
nix-env -f https://downloads.digitallyinduced.com/ihp-new.tar.gz -i ihp-new
# Or this if you use nix profile:
nix profile install -f https://downloads.digitallyinduced.com/ihp-new.tar.gz
```
--------------------------------
### Check Nix Installation
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown
Verify that Nix is installed and accessible on your system's PATH. This is a prerequisite for IHP development.
```bash
nix --version
```
--------------------------------
### Example Script: Hello World to All Users
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown
Fetches all users and prints a personalized greeting to the console. Requires the 'User' type to be defined.
```haskell
#!/usr/bin/env run-script
module Application.Script.HelloWorldToAllUsers where
import Application.Script.Prelude
run :: Script
run = do
users <- query @User |> fetch
forEach users
\user -> do
putStrLn $ "Hello World, " <> user.firstname <> "!"
```
--------------------------------
### Replace Welcome Controller with Custom Start Page
Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-welcome/README.md
To remove the default welcome controller for production, replace `startPage WelcomeAction` with your own custom start page action, such as `ProjectsAction`. Ensure the `ihp-welcome` dependency and import are also removed.
```haskell
instance FrontController WebApplication where
controllers =
[ startPage ProjectsAction -- Your own start page
-- ... other controllers
]
```
--------------------------------
### Start GHCi in IHP Project
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/debugging.markdown
Start the GHCi interactive interpreter from your project directory. Ensure `devenv up` is running in another terminal. IHP's `.ghci` configuration automatically loads necessary extensions and modules.
```bash
ghci
```
--------------------------------
### Controller Authentication Example
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown
Illustrates using `beforeAction` for authentication. `ensureIsUser` is called before the `ShowPostAction` to protect access.
```haskell
instance Controller PostsController where
beforeAction = ensureIsUser
action ShowPostAction { postId } = ...
```
--------------------------------
### Custom Configuration Example
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown
Defines a custom configuration builder named 'appConfig' in 'Config.hs', including development mode and SES settings.
```haskell
-- Config.hs
import IHP.Log.Types
appConfig :: ConfigBuilder
appConfig = do
option Development
option $ SES
{ accessKey = "myAccessKey"
, secretKey = "mySecretAccessKey"
, region = "eu-west-1"
}
```
--------------------------------
### robots.txt Content Example
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/seo.markdown
A sample `robots.txt` file to be placed in the `static/` directory. It specifies crawler allowances and the sitemap location.
```text
User-agent: *
Allow: /
Sitemap: https://yourdomain.com/sitemap.xml
```
--------------------------------
### Get unique results with distinctOn
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown
Use distinctOn() to retrieve unique results based on a specified column. The example fetches distinct todos based on userId.
```javascript
const userTodos = await query('todos')
.distinctOn('userId')
.fetch()
```
--------------------------------
### Set up PostgreSQL Database with Docker
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Initializes a PostgreSQL database container and imports the application schema and fixtures.
```bash
$ docker run --name app-db -e POSTGRES_PASSWORD=mysecretpassword -d postgres
# Import the Schema.sql
$ docker exec -i app-db psql -U postgres -d postgres < Application/Schema.sql
CREATE EXTENSION
CREATE TABLE
...
# Import the Fixtures.sql
$ docker exec -i app-db psql -U postgres -d postgres < Application/Fixtures.sql
ALTER TABLE
INSERT 0 1
INSERT 0 1
INSERT 0 1
...
```
--------------------------------
### Stream OpenAI Completion in IHP Controller
Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-openai/README.md
This example demonstrates how to use the `streamCompletion` function from the `IHP.OpenAI` module within an IHP controller to get AI-generated answers. Ensure your OpenAI secret key is securely managed, preferably not hardcoded.
```haskell
module Web.Controller.Questions where
import Web.Controller.Prelude
import Web.View.Questions.Index
import Web.View.Questions.New
import Web.View.Questions.Edit
import Web.View.Questions.Show
import qualified IHP.OpenAI as GPT
instance Controller QuestionsController where
action QuestionsAction = autoRefresh do
questions <- query @Question
|> orderByDesc #createdAt
|> fetch
render IndexView { .. }
action NewQuestionAction = do
let question = newRecord
|> set #question "What makes haskell so great?"
render NewView { .. }
action CreateQuestionAction = do
let question = newRecord @Question
question
|> fill @'["question"]
|> validateField #question nonEmpty
|> ifValid \case
Left question -> render NewView { .. }
Right question -> do
question <- question |> createRecord
setSuccessMessage "Question created"
fillAnswer question
redirectTo QuestionsAction
action DeleteQuestionAction { questionId } = do
question <- fetch questionId
deleteRecord question
setSuccessMessage "Question deleted"
redirectTo QuestionsAction
fillAnswer :: (?modelContext :: ModelContext) => Question -> IO (Async ())
fillAnswer question = do
-- Put your OpenAI secret key below:
let secretKey = "sk-XXXXXXXX"
-- This should be done with an IHP job worker instead of async
async do
GPT.streamCompletion secretKey (buildCompletionRequest question) (clearAnswer question) (appendToken question)
pure ()
buildCompletionRequest :: Question -> GPT.CompletionRequest
buildCompletionRequest Question { question } =
-- Here you can adjust the parameters of the request
GPT.newCompletionRequest
{ GPT.maxTokens = 512
, GPT.prompt = [trimming|
Question: ${question}
Answer:
|] }
-- | Sets the answer field back to an empty string
clearAnswer :: (?modelContext :: ModelContext) => Question -> IO ()
clearAnswer question = do
sqlExec "UPDATE questions SET answer = '' WHERE id = ?" (Only question.id)
pure ()
-- | Stores a couple of newly received characters to the database
appendToken :: (?modelContext :: ModelContext) => Question -> Text -> IO ()
appendToken question token = do
sqlExec "UPDATE questions SET answer = answer || ? WHERE id = ?" (token, question.id)
pure ()
```
--------------------------------
### Preventing Data Modification with Forms in Haskell
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/security.markdown
Use forms with POST, DELETE, or PUT methods for actions that modify data. GET requests should not have side effects. This example shows the correct way to use a form for a DELETE action.
```haskell
-- Correct: Use a form with POST for state-changing actions
[hsx|
|]
```
```haskell
-- Wrong: Using a plain link for a destructive action
-- (GET requests are not protected by SameSite=Lax)
[hsx|Delete|]
```
--------------------------------
### Navigate to Project Directory
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown
Change your current directory to the newly created 'blog' project folder.
```bash
cd blog
```
--------------------------------
### Initialize Terraform Configuration
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Run this command in the IaC/aws folder to initialize your Terraform configuration. Ensure Terraform is installed and AWS credentials are set up.
```bash
terraform init
```
--------------------------------
### Initialize NPM in Frontend Directory
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/realtime-spas.markdown
Navigate to the `Frontend` directory and run `npm init` to generate a `package.json` file. This step is necessary before installing any Node.js dependencies.
```bash
mkdir Frontend
cd Frontend # npm needs to be run from the Frontend directory
npm init
```
--------------------------------
### Install NPM Packages
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/assets.markdown
Use `npm add` to install JavaScript libraries from the NPM registry. The installed files will be located in the `node_modules/` directory.
```bash
npm add chart.js
```
--------------------------------
### Migrate Environment Variables from start script to .envrc
Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md
This diff shows how to move environment variable exports from a `start` script into the `.envrc` file. This is necessary as the `start` script is no longer used with devenv.sh.
```diff
if ! has nix_direnv_version || ! nix_direnv_version 2.3.0;
then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.3.0/direnvrc" "sha256-Dmd+j63L84wuzgyjITIfSxSD57Tx7v51DMxVZOsiUD8="
fi
use flake . --impure
if [ -f .env ]
then
set -o allexport
source .env
set +o allexport
fi
## Add the exports from your start script here:
+export SES_ACCESS_KEY="XXXX"
+export SES_SECRET_KEY="XXXX"
+export SES_REGION="us-east-1"
```
--------------------------------
### Basic IHP Project Setup in flake.nix
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/package-management.markdown
This snippet shows the basic structure for an IHP project's `flake.nix` file, including importing necessary modules and defining system-specific configurations for IHP.
```nix
{ systems = import systems; imports = [ ihp.flakeModules.default ]; perSystem = { pkgs, ... }: { ihp = { enable = true; projectPath = ./.; packages = with pkgs; [ # Native dependencies, e.g. imagemagick ]; haskellPackages = p: with p; [ # Haskell dependencies go here p.ihp base wai text ]; doJailbreakPackages = [ "google-oauth2" ]; }; }; }; }
```
--------------------------------
### Submit Form using GET Method
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/form.markdown
Override the default POST method to use GET for form submissions by setting `formMethod` to "GET" using `formForWithOptions`. This can be useful for search forms.
```haskell
renderForm :: Post -> Html
renderForm post = formForWithOptions post options [hsx||]
options :: FormContext Post -> FormContext Post
options formContext =
formContext
|> set #formMethod "GET"
```
--------------------------------
### Initialize Stripe in Config
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/stripe.markdown
Call 'initStripe' within your 'Config/Config.hs' to set up Stripe credentials and options. This should be done after other configuration options.
```haskell
module Config where
import IHP.Prelude
import IHP.Environment
import IHP.FrameworkConfig
import IHP.Stripe.Config
config :: ConfigBuilder
config = do
option Development
option (AppHostname "localhost")
initStripe
```
--------------------------------
### Mount SessionsController and Initialize Authentication
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/authentication.markdown
In `Web/FrontController.hs`, import the `Sessions` controller, mount it using `parseRoute`, and initialize authentication for your `User` type in `initContext`.
```haskell
import IHP.LoginSupport.Middleware
import Web.Controller.Sessions
-- ... inside initContext ...
initAuthentication @User
-- ... inside parseRoutes ...
parseRoute @SessionsController
```
--------------------------------
### Example Rendered Flash Message HTML
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/session.markdown
This is an example of the HTML structure generated by `renderFlashMessages` for success and error messages.
```html
{successMessage}
{errorMessage}
```
--------------------------------
### Initialize Filebase Storage in Config
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/file-storage.markdown
Import and call `initFilebaseStorage` in your `Config.hs` file. Replace `my-bucket-name` with your actual bucket name. Ensure `FILEBASE_KEY` and `FILEBASE_SECRET` environment variables are set.
```haskell
import IHP.FileStorage.Config
```
```haskell
module Config where
import IHP.Prelude
import IHP.Environment
import IHP.FrameworkConfig
import IHP.FileStorage.Config
config :: ConfigBuilder
config = do
option Development
option (AppHostname "localhost")
initFilebaseStorage "my-bucket-name"
```
--------------------------------
### Check direnv Installation
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/installation.markdown
Confirm that direnv is installed correctly. direnv is used to automatically manage your shell environment for IHP projects.
```bash
direnv --version
```
--------------------------------
### Nix Multi-Tenant Service Definitions
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Use 'let' functions to define services for multiple tenants, avoiding duplication. This example shows how to define migration and worker services for a main tenant and an additional tenant.
```nix
let
migrate-service = databaseUrl: minimumRevision: {
serviceConfig = {
Type = "oneshot";
ExecStart = self.inputs.myapp.inputs.ihp.apps."${pkgs.system}".migrate.program;
};
environment = {
DATABASE_URL = databaseUrl;
MINIMUM_REVISION = minimumRevision;
IHP_MIGRATION_DIR = "${migrationFiles}/Application/Migration/";
};
wantedBy = [ "multi-user.target" ];
after = [ "postgresql.service" ];
};
worker-service = databaseUrl: {
enable = true;
serviceConfig = {
Type = "simple";
ExecStart = "${package}/bin/RunJobs";
Restart = "on-failure";
};
environment = ihpEnv // { GHCRTS = "-A1M -N1 -qn1"; DATABASE_URL = databaseUrl; };
wantedBy = [ "multi-user.target" ];
};
in
{
# Main tenant
systemd.services.myapp-migrate = migrate-service "postgres://myapp:@127.0.0.1/myapp" "0";
systemd.services.myapp-worker = worker-service "postgres://myapp:@127.0.0.1/myapp";
# Additional tenants — just one line each
systemd.services.myapp-migrate-acme = migrate-service "postgres://myapp:@127.0.0.1/myapp_acme" "0";
systemd.services.myapp-worker-acme = worker-service "postgres://myapp:@127.0.0.1/myapp_acme";
}
```
--------------------------------
### Example Post ID Instance
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown
Shows an example of the 'Id Post' type in Haskell, representing a specific post ID as a UUID string.
```haskell
"5a8d1be2-33e3-4d3f-9812-b16576fe6a38" :: Id Post
```
--------------------------------
### Build unoptimized binary with Nix
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Use this command to build an unoptimized binary. This is useful for faster development cycles and testing.
```bash
nix build .#unoptimized-prod-server
```
--------------------------------
### Define Custom Start Page
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown
Customize the application's start page by providing a specific action to the startPage function in Web/FrontController.hs. Ensure to remove any default startPage definitions.
```haskell
instance FrontController WebApplication where
controllers =
[ startPage ProjectsAction
-- Generator Marker
]
```
--------------------------------
### Get Optional Parameter with Default Value
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/controller.markdown
Use `paramOrDefault` to get a parameter's value, falling back to a default if it's missing or invalid. Specify the type with `@Type`.
```haskell
action UsersAction = do
let maxItems = paramOrDefault @Int 50 "maxItems"
```
--------------------------------
### Initialize Filebase File Storage
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/config.markdown
Sets up file storage to use the Filebase IPFS-backed service. Requires Filebase access and secret keys, and the bucket name.
```haskell
-- Config.hs
import IHP.FileStorage.Config
config = do
initFilebaseStorage "my-bucket-name"
```
--------------------------------
### View Running Processes
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Use the 'top' command to observe real-time process activity and resource consumption.
```bash
top
```
--------------------------------
### Curl Command to Show a Single Post
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/json-api.markdown
Example `curl` command to show a single post by its ID. Replace the example UUID with the actual post ID. Assumes the API is running on localhost:8000.
```bash
# Show a single post
curl http://localhost:8000/ShowPost?postId=5a8a4c5e-1b5e-4b2a-9c0a-3e5e4b8e1b5e
```
--------------------------------
### Passkey Registration - Begin
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Initiates the passkey registration process by generating a challenge and returning credential options.
```APIDOC
## POST /auth/begin-passkey-registration
### Description
Generates a challenge and returns credential options for initiating passkey registration. This is the first step in the registration flow.
### Method
POST
### Endpoint
/auth/begin-passkey-registration
### Parameters
#### Request Body
- **email** (string) - Required - The email address of the user.
### Request Example
```json
{
"email": "user@example.com"
}
```
### Response
#### Success Response (200)
- **challenge** (string) - The generated challenge for registration.
- **rpId** (string) - The Relying Party identifier.
- **user** (object) - User information including ID, name, and display name.
- **id** (string) - User's unique identifier.
- **name** (string) - User's name.
- **displayName** (string) - User's display name.
- **excludeCredentials** (array) - A list of credentials to exclude during registration.
- **id** (string) - Credential ID.
- **type** (string) - Credential type.
- **transports** (array) - Allowed transports for the credential.
#### Response Example
```json
{
"challenge": "some_challenge_string",
"rpId": "your_rp_id.com",
"user": {
"id": "user_uuid",
"name": "user@example.com",
"displayName": "User Name"
},
"excludeCredentials": [
{
"id": "existing_credential_id",
"type": "public-key",
"transports": ["internal", "usb", "nfc", "ble"]
}
]
}
```
```
--------------------------------
### Deploying with Nix Run
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Commands to update flake dependencies and deploy to production using the defined flake app. `--use-substitutes` enables pulling pre-built packages, and `--build-host` specifies where the build occurs.
```bash
nix flake update # Pull latest app versions
nix run # Deploy to production
```
--------------------------------
### Apply Initial Schema to Database (Bash)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Applies the initial SQL schema to a specific database using psql. This command is typically run after NixOS has provisioned the database.
```bash
sudo -u postgres psql secondapp < /path/to/Application/Schema.sql
```
--------------------------------
### Controller Data Structure Examples
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown
Controller data structures should end in 'Controller' and typically represent actions related to a plural resource. Examples include CRUD operations for Users, Companies, Admins, and session management.
```haskell
data UsersController
= UsersAction
| NewUserAction
| ShowUserAction { userId :: !(Id User) }
| CreateUserAction
| EditUserAction { userId :: !(Id User) }
| UpdateUserAction { userId :: !(Id User) }
| DeleteUserAction { userId :: !(Id User) }
deriving (Eq, Show, Data)
data CompaniesController
= CompaniesAction
| NewCompanyAction
| ShowCompanyAction { companyId :: !(Id Company) }
| CreateCompanyAction
| EditCompanyAction { companyId :: !(Id Company) }
| UpdateCompanyAction { companyId :: !(Id Company) }
| DeleteCompanyAction { companyId :: !(Id Company) }
deriving (Eq, Show, Data)
data AdminsController
= AdminsAction
| NewAdminAction
| ShowAdminAction { adminId :: !(Id Admin) }
| CreateAdminAction
| EditAdminAction { adminId :: !(Id Admin) }
| UpdateAdminAction { adminId :: !(Id Admin) }
| DeleteAdminAction { adminId :: !(Id Admin) }
deriving (Eq, Show, Data)
data SessionsController
= NewSessionAction
| CreateSessionAction
| DeleteSessionAction
deriving (Eq, Show, Data)
```
```haskell
data CompanyController -- Type should be plural in this case
= CompaniesAction
| NewCompanyAction
| ShowCompanyAction { companyId :: !(Id Company) }
| CreateCompanyAction
| EditCompanyAction { companyId :: !(Id Company) }
| UpdateCompanyAction { companyId :: !(Id Company) }
| DeleteCompanyAction { companyId :: !(Id Company) }
deriving (Eq, Show, Data)
```
--------------------------------
### Passkey Authentication - Begin
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Initiates the passkey authentication process by generating a challenge and returning credential requests.
```APIDOC
## POST /auth/begin-passkey-authentication
### Description
Initiates the passkey authentication process. It generates a challenge and returns the necessary options for the client to request a credential assertion.
### Method
POST
### Endpoint
/auth/begin-passkey-authentication
### Parameters
#### Request Body
- **email** (string) - Required - The email address of the user attempting to authenticate.
### Request Example
```json
{
"email": "user@example.com"
}
```
### Response
#### Success Response (200)
- **challenge** (string) - The generated challenge for authentication.
- **rpId** (string) - The Relying Party identifier.
- **allowCredentials** (array) - A list of allowed credentials for this user.
- **id** (string) - Credential ID.
- **type** (string) - Credential type.
- **transports** (array) - Allowed transports for the credential.
#### Response Example
```json
{
"challenge": "auth_challenge_string",
"rpId": "your_rp_id.com",
"allowCredentials": [
{
"id": "user_credential_id",
"type": "public-key",
"transports": ["internal", "usb", "nfc", "ble"]
}
]
}
```
```
--------------------------------
### Manually Create Database User and Database (Bash)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Manually creates a PostgreSQL user and a database owned by that user. This is an alternative to declarative provisioning.
```bash
ssh production
sudo -u postgres createuser secondapp
sudo -u postgres createdb -O secondapp secondapp
```
--------------------------------
### Adjust macOS Open File Limit in start script
Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md
On macOS, append this block to the start script to increase the maximum number of open files, which is necessary for IHP to run well due to wai-static-middleware's file handle behavior.
```bash
# On macOS the default max count of open files is 256. IHP needs atleast 1024 to run well.
#
# The wai-static-middleware sometimes doesn't close it's file handles directly (likely because of it's use of lazy bytestrings)
# and then we usually hit the file limit of 256 at some point. With 1024 the limit is usually never hit as the GC kicks in earlier
# and will close the remaining lazy bytestring handles.
if [[ $OSTYPE == 'darwin'* ]]; then
ulimit -n 4096
fi
```
--------------------------------
### Build optimized production binary with Nix
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Use this command to build an optimized production-ready binary for your IHP application. This is recommended for deployment.
```bash
nix build .#optimized-prod-server
```
--------------------------------
### Get User Notifications API
Source: https://github.com/digitallyinduced/ihp/blob/master/AGENTS.md
Retrieves a list of notifications for a specific user.
```APIDOC
## GET /api/users/{userId}/notifications
### Description
Fetches all notifications associated with a given user ID.
### Method
GET
### Endpoint
/api/users/{userId}/notifications
### Parameters
#### Path Parameters
- **userId** (Int32) - Required - The unique identifier of the user.
### Response
#### Success Response (200)
- **notifications** (Vector) - A list of notifications, where each notification is a tuple containing:
- **notificationId** (Int32) - The unique ID of the notification.
- **message** (Text) - The content of the notification.
- **isRead** (Bool) - Indicates if the notification has been read.
#### Response Example
```json
{
"notifications": [
[1, "Your order has been shipped", false],
[2, "New message from support", false]
]
}
```
```
--------------------------------
### Create and Schedule Background Jobs
Source: https://context7.com/digitallyinduced/ihp/llms.txt
Demonstrates creating background jobs from controllers. Jobs can be run immediately using `create` or scheduled for future execution using `set #runAt` with a specified `runTime`. Requires `Web.Controller.Prelude`.
```haskell
-- Creating jobs from controllers
action SignupAction = do
user <- newRecord @User |> fill @'["email"] |> createRecord
-- Run job immediately
newRecord @SendEmailJob
|> set #userId user.id
|> set #template "welcome"
|> create
redirectTo DashboardAction
-- Schedule job for future execution
action ScheduleReminderAction = do
now <- getCurrentTime
let runTime = addUTCTime (nominalDay * 7) now -- 7 days from now
newRecord @SendReminderJob
|> set #userId currentUserId
|> set #runAt runTime
|> create
setSuccessMessage "Reminder scheduled"
```
--------------------------------
### Begin Passkey Registration
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Generates a challenge and returns credential options for passkey registration. Handles both new users (validating email) and existing users (using their info and excluding existing credentials). Requires `BeginPasskeyRegistrationPayload` for JSON parsing and uses session storage for challenge, email, and user ID.
```haskell
-- Step 1 of registration: generate challenge, return credential options
action BeginPasskeyRegistrationAction = do
payloadResult <- parseJsonBody @BeginPasskeyRegistrationPayload
payload <- case payloadResult of
Left errorMessage -> jsonError status400 errorMessage
Right payload -> pure payload
-- For logged-in users adding another passkey, use their existing info.
-- For new users, validate the email and generate a new user ID.
(pendingUserId, email, excludeCredentials) <- case currentUserOrNothing @User of
Just user -> do
existingPasskeys <- query @Passkey
|> filterWhere (#userId, user.id)
|> fetch
pure ( user.id
, user.email
, map passkeyCredentialDescriptor existingPasskeys
)
Nothing -> do
email <- requireAvailableEmail payload.email
userUuid <- sqlQueryScalar "SELECT uuid_generate_v4()" ()
pure (Id userUuid, email, [])
challenge <- liftIO generateChallenge
setSession registrationChallengeSessionKey (unChallenge challenge)
setSession registrationEmailSessionKey email
setSession registrationUserIdSessionKey (inputValue pendingUserId)
renderJson
(WebAuthnJson.wjEncodeCredentialOptionsRegistration
(registrationCredentialOptions challenge pendingUserId
email excludeCredentials))
```
--------------------------------
### PostsController Definition
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown
Example definition of a PostsController with various actions, including those with parameters.
```haskell
data PostsController
= PostsAction
| NewPostAction
| ShowPostAction { postId :: !(Id Post) }
| CreatePostAction
| EditPostAction { postId :: !(Id Post) }
| UpdatePostAction { postId :: !(Id Post) }
| DeletePostAction { postId :: !(Id Post) }
```
--------------------------------
### Install ImageMagick Dependency
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/file-storage.markdown
Add `imagemagick` to `otherDeps` in `default.nix` to use ImageMagick preprocessor.
```nix
otherDeps = p: with p; [
imagemagick
];
```
--------------------------------
### Get User Details API
Source: https://github.com/digitallyinduced/ihp/blob/master/AGENTS.md
Retrieves detailed information for a specific user by their ID.
```APIDOC
## GET /api/users/{userId}
### Description
Retrieves the details of a user specified by their unique ID.
### Method
GET
### Endpoint
/api/users/{userId}
### Parameters
#### Path Parameters
- **userId** (Int32) - Required - The unique identifier of the user.
### Response
#### Success Response (200)
- **name** (Text) - The name of the user.
- **email** (Text) - The email address of the user.
- **phone** (Maybe Text) - The phone number of the user, if provided.
- **isActive** (Bool) - Indicates if the user account is active.
#### Response Example
```json
{
"name": "Test User",
"email": "test@example.com",
"phone": "+1234567890",
"isActive": true
}
```
```
--------------------------------
### Nix Flake App for Server Deployment
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Define a deploy command as a flake app in your server's `flake.nix` for managing NixOS configurations. This example sets up a production configuration and a default app to deploy it.
```nix
{
outputs = { self, nixpkgs, ... }: {
nixosConfigurations.production = nixpkgs.lib.nixosSystem {
system = "aarch64-linux";
specialArgs = { inherit self; };
modules = [
./hardware-configuration.nix
./configuration.nix
./postgres.nix
./myapp.nix
./secondapp.nix
];
};
apps.aarch64-darwin.default = {
type = "app";
program = let pkgs = nixpkgs.legacyPackages.aarch64-darwin; in
"${pkgs.writeShellApplication {
name = "deploy";
text = ''
set -euo pipefail
exec ${pkgs.nixos-rebuild}/bin/nixos-rebuild switch \
--use-substitutes \
--fast \
--flake ${self}#production \
--target-host root@production \
--build-host root@production
'';
}}/bin/deploy";
};
};
}
```
--------------------------------
### Using isEmail Validator
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/validation.markdown
Example of using the `isEmail` validator for an email text field.
```haskell
|> validateField #email isEmail
```
--------------------------------
### Generate Registration Credential Options
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Generates options for `navigator.credentials.create()` to initiate passkey registration. Includes Relying Party details, user information, supported key parameters, and exclusion lists.
```haskell
registrationCredentialOptions ::
(?request :: Request) =>
Challenge ->
Id User ->
Text ->
[CredentialDescriptor] ->
CredentialOptions 'Registration
registrationCredentialOptions challenge userId displayName excludeCredentials =
CredentialOptionsRegistration
{
corRp = CredentialRpEntity
{
creId = Just (RpId rpIdTextFromRequest)
,
creName = passkeyRelyingPartyName
}
,
corUser = CredentialUserEntity
{
cueId = userHandleForUserId userId
,
cueDisplayName = UserAccountDisplayName displayName
,
cueName = UserAccountName displayName
}
,
corChallenge = challenge
,
corPubKeyCredParams =
[
CredentialParameters CredentialTypePublicKey
(CoseSignAlgECDSA CoseHashAlgECDSASHA256) -- ES256
,
CredentialParameters CredentialTypePublicKey
CoseSignAlgEdDSA -- EdDSA
,
CredentialParameters CredentialTypePublicKey
(CoseSignAlgRSA CoseHashAlgRSASHA256) -- RS256
]
,
corTimeout = Just (Timeout 60000) -- 60 seconds
,
corExcludeCredentials = excludeCredentials
,
corAuthenticatorSelection = Just AuthenticatorSelectionCriteria
{
ascAuthenticatorAttachment = Nothing
,
ascResidentKey = ResidentKeyRequirementPreferred
,
ascUserVerification = UserVerificationRequirementPreferred
}
,
corAttestation = AttestationConveyancePreferenceNone
,
corExtensions = Nothing
}
```
--------------------------------
### SQL for deleting all records
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown
Example SQL generated by `deleteAll` for removing all records from a table.
```sql
DELETE FROM users
```
--------------------------------
### SQL for updating a record
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown
Example SQL generated by `updateRecord` to modify a specific record.
```sql
UPDATE users SET firstname = firstname, lastname = "Tester" WHERE id = "cf633b17-c409-4df5-a2fc-8c3b3d6c2ea7"
```
--------------------------------
### Using isPhoneNumber Validator
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/validation.markdown
Example of using the `isPhoneNumber` validator for a phone number text field.
```haskell
|> validateField #phoneNumber isPhoneNumber
```
--------------------------------
### Load and Run Tests in GHCI
Source: https://github.com/digitallyinduced/ihp/blob/master/CONTRIBUTING.md
Use this command to load the test suite into GHCI and run the main test function. Ensure you have run 'direnv allow' if this is your first time in the IHP directory.
```bash
ghci
:l Test/Main.hs
main
```
--------------------------------
### SQL for creating many records
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown
Example SQL generated by `createMany` for inserting multiple records.
```sql
INSERT INTO users (id, firstname, lastname)
VALUES (DEFAULT, "", ""), (DEFAULT, "", "") , (DEFAULT, "", "") ... ;
```
--------------------------------
### Run Migrations on Staging Database (Bash)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database-migrations.markdown
This bash command demonstrates how to test migrations on a staging database copy. It first dumps the production database and then restores it to the staging URL, followed by running migrations.
```bash
# Create a copy of your production database
pg_dump DATABASE_URL_PRODUCTION | psql DATABASE_URL_STAGING
# Run migrations against the copy
DATABASE_URL=DATABASE_URL_STAGING migrate
```
--------------------------------
### Get RP ID from Request
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Retrieves the Relying Party Identifier text from the current HTTP request.
```haskell
rpIdTextFromRequest :: (?request :: Request) => Text
rpIdTextFromRequest = rpIdTextFromHost requestHostText
```
--------------------------------
### Fetching Paginated Records
Source: https://context7.com/digitallyinduced/ihp/llms.txt
Example of implementing pagination by limiting the number of results and skipping a certain offset.
```haskell
-- Pagination
paginatedPosts <- query @Post
|> orderByDesc #createdAt
|> limit 10
|> offset 20
|> fetch
-- SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 20
```
--------------------------------
### Example Post Record Instance
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown
Illustrates how a 'Post' record might be represented in Haskell, showing a sample UUID and text values for title and body.
```haskell
Post { id = cfefdc6c-c097-414c-91c0-cbc9a79fbe4f, title = "Hello World", body = "Some body text" } :: Post
```
--------------------------------
### Database Table Names (Bad Examples)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown
Avoid non-plural, non-snake_case, or uppercase names for database tables.
```haskell
post -- Not in plural form, should be `posts`
UserProjects -- Not in snake case, should be `user_projects`
Posts -- Should be lowercase `posts`
company -- Should be `companies`
```
--------------------------------
### Run Migrations via Nix from GitHub Actions
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database-migrations.markdown
For deployments using GitHub Actions, you can execute migrations by running the `migrate` Nix target.
```bash
nix run .#migrate
```
--------------------------------
### SQL for deleting many records
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown
Example SQL generated by `deleteRecords` or `deleteRecordByIds` for removing multiple records.
```sql
DELETE FROM users WHERE id IN (...)
```
--------------------------------
### SQL for deleting a single record
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/database.markdown
Example SQL generated by `deleteRecord` or `deleteRecordById` for removing a single record.
```sql
DELETE FROM users WHERE id = "cf633b17-c409-4df5-a2fc-8c3b3d6c2ea7"
```
--------------------------------
### Initial flake.nix Configuration
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/package-management.markdown
This is the default configuration for `flake.nix` before adding any native dependencies. It outlines the project's inputs and basic structure.
```nix
{
{
inputs = {
ihp.url = "github:digitallyinduced/ihp/1.1";
nixpkgs.follows = "ihp/nixpkgs";
flake-parts.follows = "ihp/flake-parts";
devenv.follows = "ihp/devenv";
systems.follows = "ihp/systems";
};
outputs = inputs@{ ihp, flake-parts, systems, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = import systems;
imports = [ ihp.flakeModules.default ];
perSystem = { pkgs, ... }:
{
ihp = {
enable = true;
projectPath = ./.;
packages = with pkgs; [
# Native dependencies, e.g. imagemagick
];
haskellPackages = p: with p; [
# Haskell dependencies go here
p.ihp
base
wai
text
];
};
};
};
}
}
```
--------------------------------
### Hash Password CLI Tool
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/authentication.markdown
Demonstrates how to hash a password using the `hash-password` command-line tool. This is useful for manually inserting users into the database.
```bash
$ hash-password
Enter your password and press enter:
hunter2
sha256|17|Y32Ga1uke5CisJvVp6p2sg==|TSDuEs1+Xdaels6TYCkyCgIBHxWA/US7bvBlK0vHzvc=
```
--------------------------------
### Set GitHub Environment Variables
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/oauth.markdown
Sets the OAUTH_GITHUB_CLIENT_ID and OAUTH_GITHUB_CLIENT_SECRET environment variables in the start script for local development.
```bash
# ...
export OAUTH_GITHUB_CLIENT_ID="MY_GITHUB_CLIENT_ID" # <-- ADD THIS LINE BEFORE THE RunDevServer CALL
export OAUTH_GITHUB_CLIENT_SECRET="MY_GITHUB_SECRET"
RunDevServer
```
--------------------------------
### Begin Passkey Authentication Challenge
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/passkeys.markdown
Generates a challenge for passkey authentication, stores it in the session, and renders the necessary options for the client to initiate the authentication process.
```haskell
action BeginPasskeyAuthenticationAction = do
challenge <- liftIO generateChallenge
setSession authenticationChallengeSessionKey (unChallenge challenge)
renderJson
(WebAuthnJson.wjEncodeCredentialOptionsAuthentication
(authenticationCredentialOptions challenge))
```
--------------------------------
### Declarative PostgreSQL Database and User Provisioning
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/deployment.markdown
Automatically create databases and users for your applications using NixOS. This configuration ensures that specified databases and users exist and that users own their corresponding databases.
```nix
services.postgresql = {
ensureDatabases = [ "myapp" "secondapp" ];
ensureUsers = [
{ name = "myapp"; ensureDBOwnership = true; }
{ name = "secondapp"; ensureDBOwnership = true; }
];
};
```
--------------------------------
### Label Results (Indexed Many-to-Many) (Before)
Source: https://github.com/digitallyinduced/ihp/blob/master/UPGRADE.md
Example using the removed `labelResults` function for indexed many-to-many relationships with QueryBuilder.
```haskell
-- Before
labeledPosts <- query @Post
|> innerJoin @Tagging (#id, #postId)
|> innerJoinThirdTable @Tag @Tagging (#id, #tagId)
|> labelResults @Tag #id
|> fetch
-- labeledPosts :: [LabeledData (Id' "tags") Post]
```
--------------------------------
### Required Imports for BlazeHtml Example
Source: https://github.com/digitallyinduced/ihp/blob/master/ihp-hsx/README.md
These are the necessary imports for using BlazeHtml components and attributes within a Haskell module.
```haskell
module Web.View.Posts.Edit where
import Web.View.Prelude
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A
import Prelude as P
```
--------------------------------
### Connect to PostgreSQL and Query Table
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/your-first-project.markdown
Connect to the local PostgreSQL database using the 'make psql' command. Once connected, you can run SQL queries to inspect the database, such as selecting all rows from the 'posts' table. Type '\q' to quit.
```bash
$ make psql
psql (9.6.12)
Type "help" for help.
-- Let's do a query to check that the table is there
app=# SELECT * FROM posts;
id | title | body
----+-------+------
(0 rows)
-- Looks alright! :)
-- We can quit the PostgreSQL console by typing \q
app=# \q
```
--------------------------------
### AutoRoute with Multiple Parameters
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/routing.markdown
Example of an action constructor with multiple parameters, demonstrating how AutoRoute generates query parameters for each.
```haskell
data PostsController = EditPostAction { postId :: !(Id Post), userId :: !(Id User) }
GET /EditPost?postId={postId}&userId={userId} => EditPostAction { postId, userId }
```
--------------------------------
### Database Column Names (Bad Examples)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown
Avoid camelCase for database column names; use snake_case instead.
```sql
isConfirmed -- Not in snake case, should be `is_confirmed`
firstName -- Not in snake case, should be `first_name`
```
--------------------------------
### Database Table Names (Good Examples)
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/naming-conventions.markdown
Use lowercase, snake_case, and plural form for database table names.
```haskell
users
projects
companies
user_projects
company_admins
reactions
invites
comments
```
--------------------------------
### Build Production Server with Scripts
Source: https://github.com/digitallyinduced/ihp/blob/master/Guide/scripts.markdown
Command to build all IHP scripts along with the application for production deployment using Nix.
```bash
# Build all scripts along with the application
nix build .#optimized-prod-server
# Or for faster, unoptimized builds
nix build .#unoptimized-prod-server
```