{"owner":"digitallyinduced","repo":"ihp","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Overview\n\nIHP (Integrated Haskell Platform) is a batteries-included Haskell web framework built on Haskell and Nix. It follows an MVC architecture with type-safe routing, HSX templating (JSX-like syntax for HTML), and PostgreSQL as the database.\n\n## Development Environment Setup\n\nIHP uses devenv.sh (a wrapper around `nix develop` and `direnv`) for development:\n\n```bash\ncd /path/to/ihp\ndirenv allow\n```\n\nAlternatively, use `nix develop` directly to enter a dev shell. You can also use `direnv exec .` to get a cached nix develop environment.\n\n## Running Tests\n\nPrefer `ghci` for quick type checking and iteration — full nix builds (`nix flake check`) are slow. Use ghci interactively or via `echo ... | ghci` one-liners for fast feedback.\n\n**When using ghci, keep in mind that it expects input on stdin. If you don't provide any, ghci will never return.** The `echo ... | ghci` pattern handles this correctly by piping input.\n\n**IHP IDE Tests** (from the repo root):\n```bash\n# Interactive (allows :r to reload after changes):\nghci\n:l ihp-ide/Test/Main.hs\nmain\n:r\nmain\n\n# One-liner:\necho -e ':l ihp-ide/Test/Main.hs\\nmain' | ghci\n```\n\n**All tests via Nix** (slower but comprehensive):\n```bash\nnix flake check --impure\n```\n\n**Verify a single module compiles:**\n```bash\necho ':l ihp-ide/IHP/IDE/CodeGen/MigrationGenerator.hs' | ghci\n```\n\nWhen adding new tests, add them to `Test/Main.hs` in the appropriate package.\n\n## Running the Dev Server (for local IHP development)\n\nFrom the IHP directory with a host project:\n```bash\nexport DEBUG=1  # Optional: enable debug logging\nghci\n:l ihp-ide/exe/IHP/IDE/DevServer.hs\nmainInParentDirectory\n```\n\n## Project Structure\n\nThis is a monorepo with multiple Haskell packages:\n\n| Package | Purpose |\n|---------|---------|\n| `ihp/` | Core web framework (routing, controllers, views, models, validation) |\n| `ihp-ide/` | Development server, code generators, schema designer |\n| `ihp-hsx/` | HSX templating system (JSX-like HTML in Haskell) |\n| `ihp-migrate/` | Database migration tool |\n| `ihp-datasync/` | Real-time data synchronization via WebSockets |\n| `ihp-graphql/` | GraphQL API support |\n| `ihp-hspec/` | Testing utilities |\n| `ihp-openai/` | OpenAI integration |\n| `ihp-ssc/` | Server-side components |\n\n## Core Framework Architecture (`ihp/IHP/`)\n\n- **Prelude modules**: `Prelude.hs`, `ControllerPrelude.hs`, `ViewPrelude.hs`, `RouterPrelude.hs`, `MailPrelude.hs` - standard imports for different contexts\n- **Server.hs**: HTTP server initialization and middleware\n- **FrameworkConfig.hs**: Framework configuration\n- **RouterSupport.hs**: Type-safe routing and URL generation\n- **QueryBuilder.hs**: Type-safe SQL query builder\n- **ModelSupport.hs**: ORM-like database model support\n- **Controller/**: Request handling, params, sessions, cookies, redirects\n- **View/**: Form helpers, CSS framework integration, rendering\n- **ValidationSupport/**: Field validation\n- **AuthSupport/**: Authentication\n- **LoginSupport/**: Login functionality\n- **Job/**: Background job queue and runner\n- **FileStorage/**: File upload abstraction\n- **AutoRefresh.hs**: Real-time view updates via WebSocket\n- **Mail.hs**: Email composition and sending\n\n## IDE/Code Generation (`ihp-ide/`)\n\n- **exe/IHP/IDE/DevServer.hs**: Development server entry point\n- **IHP/IDE/SchemaDesigner/**: Visual database schema editor\n- **IHP/IDE/CodeGen/**: Code generators for controllers, views, migrations, jobs, etc.\n- **IHP/SchemaCompiler.hs**: Generates `Types.hs` from SQL schema\n\n## Code Guidelines\n\n- Use `pure` instead of `return` (clearer for non-Haskell developers)\n- Add Haddock comments to public APIs\n- The framework uses implicit parameters extensively (see `ImplicitParams` extension)\n- HSX uses quasiquotes: `[hsx|<div>content</div>|]`\n\n## Git Workflow\n\n- **Never force push.** If a push is rejected because the remote has advanced, `git fetch` and rebase (or merge) onto the updated remote, then push normally. Do not use `git push --force` or `--force-with-lease`.\n\n## Hasql Database Patterns (Reference)\n\nBased on patterns from [hasql-tutorial1](https://github.com/nikita-volkov/hasql-tutorial1). Hasql provides type-safe PostgreSQL access with a layered architecture.\n\n### Module Structure\n\nThe tutorial demonstrates a library-oriented structure where database code is isolated into a reusable package:\n\n```\nmy-db-library/\n├── my-db-library.cabal\n└── library/\n    └── MyDbLibrary/\n        ├── Prelude.hs      -- Re-exports (rerebase), internal\n        ├── Statement.hs    -- Raw SQL statements, internal\n        ├── Transaction.hs  -- Business logic composition, internal\n        └── Session.hs      -- PUBLIC API (only exposed module)\n```\n\n**Cabal exposure pattern:**\n```cabal\nlibrary\n  exposed-modules:\n    MyDbLibrary.Session           -- Only public module\n  other-modules:\n    MyDbLibrary.Prelude           -- Internal\n    MyDbLibrary.Statement         -- Internal\n    MyDbLibrary.Transaction       -- Internal\n```\n\n**Why this structure?**\n- **Stable API**: Only `Session` is exposed, allowing internal refactoring without breaking consumers\n- **Encapsulation**: Statements and transactions are implementation details\n- **Reusability**: The library can be shared across multiple apps (CLI, REST API, etc.)\n- **Testability**: Each layer can be tested independently\n\n### Import Pattern\n\n```haskell\n-- In Statement.hs (internal)\nmodule MyDbLibrary.Statement where\n\nimport MyDbLibrary.Prelude\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\n-- In Transaction.hs (internal)\nmodule MyDbLibrary.Transaction where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Statement as Statement\nimport qualified Hasql.Transaction as Transaction\n\n-- In Session.hs (public API)\nmodule MyDbLibrary.Session where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Transaction as Transaction\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n```\n\n### Layer 1: Statements (Raw SQL + Codecs)\n\nStatements combine SQL with type-safe encoders/decoders:\n\n```haskell\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\nfindUserByEmail :: Hasql.Statement Text (Maybe Int32)\nfindUserByEmail =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id FROM users WHERE email = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.text)\n    decoder = Decoders.rowMaybe (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ninsertUser :: Hasql.Statement (Text, ByteString, Text, Maybe Text) Int32\ninsertUser =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"INSERT INTO users (email, password, name, phone) VALUES ($1, $2, $3, $4) RETURNING id\"\n    encoder =\n      contramap (\\(a,b,c,d) -> (a,(b,(c,d)))) $\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nullable Encoders.text)\n    decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ngetUserNotifications :: Hasql.Statement Int32 (Vector (Int32, Text, Bool))\ngetUserNotifications =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id, message, read FROM notifications WHERE user_id = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.int4)\n    decoder = Decoders.rowVector $\n      (,,) <$>\n        Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n        Decoders.column (Decoders.nonNullable Decoders.text) <*>\n        Decoders.column (Decoders.nonNullable Decoders.bool)\n```\n\nKey points:\n- `$1`, `$2` placeholders for parameters (PostgreSQL prepared statements)\n- Encoder maps Haskell tuple → PostgreSQL binary format\n- Decoder maps PostgreSQL result → Haskell type\n- Last `Bool` parameter enables statement caching\n- **Codecs stay with SQL**: Encoders/decoders defined in same module as queries\n\n### Layer 2: Transactions (Business Logic)\n\nTransactions compose statements with atomicity:\n\n```haskell\nimport qualified Hasql.Transaction as Transaction\nimport qualified Hasql.Transaction.Sessions as Sessions\n\nregister :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister email password name phone = do\n  existingId <- Transaction.statement email Statement.findUserByEmail\n  case existingId of\n    Just id -> pure (False, id)  -- User already existed\n    Nothing -> do\n      newId <- Transaction.statement (email, password, name, phone) Statement.insertUser\n      pure (True, newId)         -- New user created\n```\n\nThe `(Bool, Int32)` return type is idempotent: callers know whether the operation created a new record.\n\n**Alternative: Applicative/Selective composition** (for independent operations):\n```haskell\nimport Control.Selective (fromMaybeS)\n\nregister' :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister' email password name phone =\n  fromMaybeS\n    (fmap (\\newId -> (True, newId)) $\n      Transaction.statement (email, password, name, phone) Statement.insertUser)\n    (fmap (\\existingId -> (False, existingId)) $\n      Transaction.statement email Statement.findUserByEmail)\n```\n\n### Layer 3: Sessions (Public API)\n\nSessions wrap transactions and manage connections:\n\n```haskell\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n\n-- Using transactions (for multi-statement operations)\nregisterUser :: Text -> ByteString -> Text -> Maybe Text -> Session (Bool, Int32)\nregisterUser email password name phone =\n  Sessions.transaction Sessions.Write Sessions.Serializable $\n    Transaction.register email password name phone\n\n-- Direct statement (for single-statement operations)\nauthenticate :: Text -> ByteString -> Session (Maybe (Bool, Int32))\nauthenticate email password =\n  Session.statement (email, password) Statement.authenticateUser\n\ngetUserDetails :: Int32 -> Session (Maybe (Text, Text, Maybe Text, Bool))\ngetUserDetails userId =\n  Session.statement userId Statement.getUserDetails\n\ngetNotifications :: Int32 -> Session (Vector (Int32, Text, Bool))\ngetNotifications userId =\n  Session.statement userId Statement.getUserNotifications\n```\n\nTransaction modes:\n- `Write Serializable`: For critical operations (registration, payments) - full isolation\n- `Read ReadCommitted`: For queries - weaker isolation, better concurrency\n\n### Result Type Conventions\n\n| Pattern | Type | Use Case |\n|---------|------|----------|\n| Optional row | `Maybe a` | `SELECT` returning 0 or 1 row |\n| Multiple rows | `Vector a` | `SELECT` returning many rows |\n| Multi-column | `(a, b, c)` | Tuples, not custom wrapper types |\n| Success flag | `Bool` | `UPDATE`/`DELETE` success |\n| Idempotent | `(Bool, a)` | Indicates if operation was performed |\n\n**Why tuples over custom types?**\n- Less boilerplate\n- Results are internal to the library anyway\n- Consumers see semantic Session API, not raw tuples\n\n### Encoder Composition\n\nFor multi-parameter statements, compose encoders with `<>`:\n\n```haskell\n-- For (Text, ByteString, Int32)\nencoder =\n  contramap (\\(a,b,c) -> (a,(b,c))) $\n    Encoders.param (Encoders.nonNullable Encoders.text) <>\n    Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n    Encoders.param (Encoders.nonNullable Encoders.int4)\n```\n\nThe `contramap` restructures the flat tuple into nested pairs for the `<>` combinator.\n\n### Decoder Composition\n\n```haskell\n-- Single optional row with multiple columns\nDecoders.rowMaybe $\n  (,,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Vector of rows (multiple results)\nDecoders.rowVector $\n  (,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Single required row\nDecoders.singleRow $\n  Decoders.column (Decoders.nonNullable Decoders.int4)\n```\n\n### Dependencies\n\n```cabal\nbuild-depends:\n    hasql >= 1.4 && < 1.5\n  , hasql-transaction >= 0.10 && < 0.11\n  , rerebase >= 1.4 && < 1.5  -- Enhanced prelude with Text, Vector, etc.\n```\n\nNote: `hasql-transaction` is marked experimental but widely used.\n\n## Key Language Extensions\n\nThe codebase uses GHC2021 with these notable extensions:\n- `ImplicitParams`: For passing context implicitly\n- `OverloadedLabels`: For field access (`#fieldName`)\n- `QuasiQuotes`: For HSX templates\n- `TypeFamilies` and `DataKinds`: For type-safe routing and models\n\n## Faster Development Builds\n\nFor faster rebuilds during development, uncomment the `configureFlags = [ \"--flag FastBuild\" ];` line in `ihp.nix`. Don't commit this change:\n```bash\ngit update-index --assume-unchanged ihp.nix\n```\n\n## Working with a Local IHP in an App\n\nClone IHP into your app directory, then patch `flake.nix`:\n```bash\ngit clone git@github.com:digitallyinduced/ihp.git IHP\ncd IHP\nsed -i \"s|ihp.url = .*|ihp.url = \\\"path://$(pwd)\\\";|\" ../flake.nix\ndirenv allow\ncd ..\nnix flake update\n```\n\n## Documentation\n\nThe `/Guide/` directory contains comprehensive documentation in markdown. Run `devenv up` from the Guide directory to preview changes locally.\n\nThe production version of the documentation is hosted at: ihp.digitallyinduced.com\n\nTo update it:\n- `cd ~/digitallyinduced/ihp-website && nix flake update ihpDocs`. Then commit the lock file change\n- Next deploy the nixos configuration: `cd ~/digitallyinduced/servers && nix flake update ihpWebsite && nix run .#deploy`\n\nWhen a new version of IHP is released, also update the ihp-website repo\n"},"files":{"CLAUDE.md":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Overview\n\nIHP (Integrated Haskell Platform) is a batteries-included Haskell web framework built on Haskell and Nix. It follows an MVC architecture with type-safe routing, HSX templating (JSX-like syntax for HTML), and PostgreSQL as the database.\n\n## Development Environment Setup\n\nIHP uses devenv.sh (a wrapper around `nix develop` and `direnv`) for development:\n\n```bash\ncd /path/to/ihp\ndirenv allow\n```\n\nAlternatively, use `nix develop` directly to enter a dev shell. You can also use `direnv exec .` to get a cached nix develop environment.\n\n## Running Tests\n\nPrefer `ghci` for quick type checking and iteration — full nix builds (`nix flake check`) are slow. Use ghci interactively or via `echo ... | ghci` one-liners for fast feedback.\n\n**When using ghci, keep in mind that it expects input on stdin. If you don't provide any, ghci will never return.** The `echo ... | ghci` pattern handles this correctly by piping input.\n\n**IHP IDE Tests** (from the repo root):\n```bash\n# Interactive (allows :r to reload after changes):\nghci\n:l ihp-ide/Test/Main.hs\nmain\n:r\nmain\n\n# One-liner:\necho -e ':l ihp-ide/Test/Main.hs\\nmain' | ghci\n```\n\n**All tests via Nix** (slower but comprehensive):\n```bash\nnix flake check --impure\n```\n\n**Verify a single module compiles:**\n```bash\necho ':l ihp-ide/IHP/IDE/CodeGen/MigrationGenerator.hs' | ghci\n```\n\nWhen adding new tests, add them to `Test/Main.hs` in the appropriate package.\n\n## Running the Dev Server (for local IHP development)\n\nFrom the IHP directory with a host project:\n```bash\nexport DEBUG=1  # Optional: enable debug logging\nghci\n:l ihp-ide/exe/IHP/IDE/DevServer.hs\nmainInParentDirectory\n```\n\n## Project Structure\n\nThis is a monorepo with multiple Haskell packages:\n\n| Package | Purpose |\n|---------|---------|\n| `ihp/` | Core web framework (routing, controllers, views, models, validation) |\n| `ihp-ide/` | Development server, code generators, schema designer |\n| `ihp-hsx/` | HSX templating system (JSX-like HTML in Haskell) |\n| `ihp-migrate/` | Database migration tool |\n| `ihp-datasync/` | Real-time data synchronization via WebSockets |\n| `ihp-graphql/` | GraphQL API support |\n| `ihp-hspec/` | Testing utilities |\n| `ihp-openai/` | OpenAI integration |\n| `ihp-ssc/` | Server-side components |\n\n## Core Framework Architecture (`ihp/IHP/`)\n\n- **Prelude modules**: `Prelude.hs`, `ControllerPrelude.hs`, `ViewPrelude.hs`, `RouterPrelude.hs`, `MailPrelude.hs` - standard imports for different contexts\n- **Server.hs**: HTTP server initialization and middleware\n- **FrameworkConfig.hs**: Framework configuration\n- **RouterSupport.hs**: Type-safe routing and URL generation\n- **QueryBuilder.hs**: Type-safe SQL query builder\n- **ModelSupport.hs**: ORM-like database model support\n- **Controller/**: Request handling, params, sessions, cookies, redirects\n- **View/**: Form helpers, CSS framework integration, rendering\n- **ValidationSupport/**: Field validation\n- **AuthSupport/**: Authentication\n- **LoginSupport/**: Login functionality\n- **Job/**: Background job queue and runner\n- **FileStorage/**: File upload abstraction\n- **AutoRefresh.hs**: Real-time view updates via WebSocket\n- **Mail.hs**: Email composition and sending\n\n## IDE/Code Generation (`ihp-ide/`)\n\n- **exe/IHP/IDE/DevServer.hs**: Development server entry point\n- **IHP/IDE/SchemaDesigner/**: Visual database schema editor\n- **IHP/IDE/CodeGen/**: Code generators for controllers, views, migrations, jobs, etc.\n- **IHP/SchemaCompiler.hs**: Generates `Types.hs` from SQL schema\n\n## Code Guidelines\n\n- Use `pure` instead of `return` (clearer for non-Haskell developers)\n- Add Haddock comments to public APIs\n- The framework uses implicit parameters extensively (see `ImplicitParams` extension)\n- HSX uses quasiquotes: `[hsx|<div>content</div>|]`\n\n## Git Workflow\n\n- **Never force push.** If a push is rejected because the remote has advanced, `git fetch` and rebase (or merge) onto the updated remote, then push normally. Do not use `git push --force` or `--force-with-lease`.\n\n## Hasql Database Patterns (Reference)\n\nBased on patterns from [hasql-tutorial1](https://github.com/nikita-volkov/hasql-tutorial1). Hasql provides type-safe PostgreSQL access with a layered architecture.\n\n### Module Structure\n\nThe tutorial demonstrates a library-oriented structure where database code is isolated into a reusable package:\n\n```\nmy-db-library/\n├── my-db-library.cabal\n└── library/\n    └── MyDbLibrary/\n        ├── Prelude.hs      -- Re-exports (rerebase), internal\n        ├── Statement.hs    -- Raw SQL statements, internal\n        ├── Transaction.hs  -- Business logic composition, internal\n        └── Session.hs      -- PUBLIC API (only exposed module)\n```\n\n**Cabal exposure pattern:**\n```cabal\nlibrary\n  exposed-modules:\n    MyDbLibrary.Session           -- Only public module\n  other-modules:\n    MyDbLibrary.Prelude           -- Internal\n    MyDbLibrary.Statement         -- Internal\n    MyDbLibrary.Transaction       -- Internal\n```\n\n**Why this structure?**\n- **Stable API**: Only `Session` is exposed, allowing internal refactoring without breaking consumers\n- **Encapsulation**: Statements and transactions are implementation details\n- **Reusability**: The library can be shared across multiple apps (CLI, REST API, etc.)\n- **Testability**: Each layer can be tested independently\n\n### Import Pattern\n\n```haskell\n-- In Statement.hs (internal)\nmodule MyDbLibrary.Statement where\n\nimport MyDbLibrary.Prelude\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\n-- In Transaction.hs (internal)\nmodule MyDbLibrary.Transaction where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Statement as Statement\nimport qualified Hasql.Transaction as Transaction\n\n-- In Session.hs (public API)\nmodule MyDbLibrary.Session where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Transaction as Transaction\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n```\n\n### Layer 1: Statements (Raw SQL + Codecs)\n\nStatements combine SQL with type-safe encoders/decoders:\n\n```haskell\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\nfindUserByEmail :: Hasql.Statement Text (Maybe Int32)\nfindUserByEmail =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id FROM users WHERE email = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.text)\n    decoder = Decoders.rowMaybe (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ninsertUser :: Hasql.Statement (Text, ByteString, Text, Maybe Text) Int32\ninsertUser =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"INSERT INTO users (email, password, name, phone) VALUES ($1, $2, $3, $4) RETURNING id\"\n    encoder =\n      contramap (\\(a,b,c,d) -> (a,(b,(c,d)))) $\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nullable Encoders.text)\n    decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ngetUserNotifications :: Hasql.Statement Int32 (Vector (Int32, Text, Bool))\ngetUserNotifications =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id, message, read FROM notifications WHERE user_id = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.int4)\n    decoder = Decoders.rowVector $\n      (,,) <$>\n        Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n        Decoders.column (Decoders.nonNullable Decoders.text) <*>\n        Decoders.column (Decoders.nonNullable Decoders.bool)\n```\n\nKey points:\n- `$1`, `$2` placeholders for parameters (PostgreSQL prepared statements)\n- Encoder maps Haskell tuple → PostgreSQL binary format\n- Decoder maps PostgreSQL result → Haskell type\n- Last `Bool` parameter enables statement caching\n- **Codecs stay with SQL**: Encoders/decoders defined in same module as queries\n\n### Layer 2: Transactions (Business Logic)\n\nTransactions compose statements with atomicity:\n\n```haskell\nimport qualified Hasql.Transaction as Transaction\nimport qualified Hasql.Transaction.Sessions as Sessions\n\nregister :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister email password name phone = do\n  existingId <- Transaction.statement email Statement.findUserByEmail\n  case existingId of\n    Just id -> pure (False, id)  -- User already existed\n    Nothing -> do\n      newId <- Transaction.statement (email, password, name, phone) Statement.insertUser\n      pure (True, newId)         -- New user created\n```\n\nThe `(Bool, Int32)` return type is idempotent: callers know whether the operation created a new record.\n\n**Alternative: Applicative/Selective composition** (for independent operations):\n```haskell\nimport Control.Selective (fromMaybeS)\n\nregister' :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister' email password name phone =\n  fromMaybeS\n    (fmap (\\newId -> (True, newId)) $\n      Transaction.statement (email, password, name, phone) Statement.insertUser)\n    (fmap (\\existingId -> (False, existingId)) $\n      Transaction.statement email Statement.findUserByEmail)\n```\n\n### Layer 3: Sessions (Public API)\n\nSessions wrap transactions and manage connections:\n\n```haskell\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n\n-- Using transactions (for multi-statement operations)\nregisterUser :: Text -> ByteString -> Text -> Maybe Text -> Session (Bool, Int32)\nregisterUser email password name phone =\n  Sessions.transaction Sessions.Write Sessions.Serializable $\n    Transaction.register email password name phone\n\n-- Direct statement (for single-statement operations)\nauthenticate :: Text -> ByteString -> Session (Maybe (Bool, Int32))\nauthenticate email password =\n  Session.statement (email, password) Statement.authenticateUser\n\ngetUserDetails :: Int32 -> Session (Maybe (Text, Text, Maybe Text, Bool))\ngetUserDetails userId =\n  Session.statement userId Statement.getUserDetails\n\ngetNotifications :: Int32 -> Session (Vector (Int32, Text, Bool))\ngetNotifications userId =\n  Session.statement userId Statement.getUserNotifications\n```\n\nTransaction modes:\n- `Write Serializable`: For critical operations (registration, payments) - full isolation\n- `Read ReadCommitted`: For queries - weaker isolation, better concurrency\n\n### Result Type Conventions\n\n| Pattern | Type | Use Case |\n|---------|------|----------|\n| Optional row | `Maybe a` | `SELECT` returning 0 or 1 row |\n| Multiple rows | `Vector a` | `SELECT` returning many rows |\n| Multi-column | `(a, b, c)` | Tuples, not custom wrapper types |\n| Success flag | `Bool` | `UPDATE`/`DELETE` success |\n| Idempotent | `(Bool, a)` | Indicates if operation was performed |\n\n**Why tuples over custom types?**\n- Less boilerplate\n- Results are internal to the library anyway\n- Consumers see semantic Session API, not raw tuples\n\n### Encoder Composition\n\nFor multi-parameter statements, compose encoders with `<>`:\n\n```haskell\n-- For (Text, ByteString, Int32)\nencoder =\n  contramap (\\(a,b,c) -> (a,(b,c))) $\n    Encoders.param (Encoders.nonNullable Encoders.text) <>\n    Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n    Encoders.param (Encoders.nonNullable Encoders.int4)\n```\n\nThe `contramap` restructures the flat tuple into nested pairs for the `<>` combinator.\n\n### Decoder Composition\n\n```haskell\n-- Single optional row with multiple columns\nDecoders.rowMaybe $\n  (,,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Vector of rows (multiple results)\nDecoders.rowVector $\n  (,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Single required row\nDecoders.singleRow $\n  Decoders.column (Decoders.nonNullable Decoders.int4)\n```\n\n### Dependencies\n\n```cabal\nbuild-depends:\n    hasql >= 1.4 && < 1.5\n  , hasql-transaction >= 0.10 && < 0.11\n  , rerebase >= 1.4 && < 1.5  -- Enhanced prelude with Text, Vector, etc.\n```\n\nNote: `hasql-transaction` is marked experimental but widely used.\n\n## Key Language Extensions\n\nThe codebase uses GHC2021 with these notable extensions:\n- `ImplicitParams`: For passing context implicitly\n- `OverloadedLabels`: For field access (`#fieldName`)\n- `QuasiQuotes`: For HSX templates\n- `TypeFamilies` and `DataKinds`: For type-safe routing and models\n\n## Faster Development Builds\n\nFor faster rebuilds during development, uncomment the `configureFlags = [ \"--flag FastBuild\" ];` line in `ihp.nix`. Don't commit this change:\n```bash\ngit update-index --assume-unchanged ihp.nix\n```\n\n## Working with a Local IHP in an App\n\nClone IHP into your app directory, then patch `flake.nix`:\n```bash\ngit clone git@github.com:digitallyinduced/ihp.git IHP\ncd IHP\nsed -i \"s|ihp.url = .*|ihp.url = \\\"path://$(pwd)\\\";|\" ../flake.nix\ndirenv allow\ncd ..\nnix flake update\n```\n\n## Documentation\n\nThe `/Guide/` directory contains comprehensive documentation in markdown. Run `devenv up` from the Guide directory to preview changes locally.\n\nThe production version of the documentation is hosted at: ihp.digitallyinduced.com\n\nTo update it:\n- `cd ~/digitallyinduced/ihp-website && nix flake update ihpDocs`. Then commit the lock file change\n- Next deploy the nixos configuration: `cd ~/digitallyinduced/servers && nix flake update ihpWebsite && nix run .#deploy`\n\nWhen a new version of IHP is released, also update the ihp-website repo\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## Overview\n\nIHP (Integrated Haskell Platform) is a batteries-included Haskell web framework built on Haskell and Nix. It follows an MVC architecture with type-safe routing, HSX templating (JSX-like syntax for HTML), and PostgreSQL as the database.\n\n## Development Environment Setup\n\nIHP uses devenv.sh (a wrapper around `nix develop` and `direnv`) for development:\n\n```bash\ncd /path/to/ihp\ndirenv allow\n```\n\nAlternatively, use `nix develop` directly to enter a dev shell. You can also use `direnv exec .` to get a cached nix develop environment.\n\n## Running Tests\n\nPrefer `ghci` for quick type checking and iteration — full nix builds (`nix flake check`) are slow. Use ghci interactively or via `echo ... | ghci` one-liners for fast feedback.\n\n**When using ghci, keep in mind that it expects input on stdin. If you don't provide any, ghci will never return.** The `echo ... | ghci` pattern handles this correctly by piping input.\n\n**IHP IDE Tests** (from the repo root):\n```bash\n# Interactive (allows :r to reload after changes):\nghci\n:l ihp-ide/Test/Main.hs\nmain\n:r\nmain\n\n# One-liner:\necho -e ':l ihp-ide/Test/Main.hs\\nmain' | ghci\n```\n\n**All tests via Nix** (slower but comprehensive):\n```bash\nnix flake check --impure\n```\n\n**Verify a single module compiles:**\n```bash\necho ':l ihp-ide/IHP/IDE/CodeGen/MigrationGenerator.hs' | ghci\n```\n\nWhen adding new tests, add them to `Test/Main.hs` in the appropriate package.\n\n## Running the Dev Server (for local IHP development)\n\nFrom the IHP directory with a host project:\n```bash\nexport DEBUG=1  # Optional: enable debug logging\nghci\n:l ihp-ide/exe/IHP/IDE/DevServer.hs\nmainInParentDirectory\n```\n\n## Project Structure\n\nThis is a monorepo with multiple Haskell packages:\n\n| Package | Purpose |\n|---------|---------|\n| `ihp/` | Core web framework (routing, controllers, views, models, validation) |\n| `ihp-ide/` | Development server, code generators, schema designer |\n| `ihp-hsx/` | HSX templating system (JSX-like HTML in Haskell) |\n| `ihp-migrate/` | Database migration tool |\n| `ihp-datasync/` | Real-time data synchronization via WebSockets |\n| `ihp-graphql/` | GraphQL API support |\n| `ihp-hspec/` | Testing utilities |\n| `ihp-openai/` | OpenAI integration |\n| `ihp-ssc/` | Server-side components |\n\n## Core Framework Architecture (`ihp/IHP/`)\n\n- **Prelude modules**: `Prelude.hs`, `ControllerPrelude.hs`, `ViewPrelude.hs`, `RouterPrelude.hs`, `MailPrelude.hs` - standard imports for different contexts\n- **Server.hs**: HTTP server initialization and middleware\n- **FrameworkConfig.hs**: Framework configuration\n- **RouterSupport.hs**: Type-safe routing and URL generation\n- **QueryBuilder.hs**: Type-safe SQL query builder\n- **ModelSupport.hs**: ORM-like database model support\n- **Controller/**: Request handling, params, sessions, cookies, redirects\n- **View/**: Form helpers, CSS framework integration, rendering\n- **ValidationSupport/**: Field validation\n- **AuthSupport/**: Authentication\n- **LoginSupport/**: Login functionality\n- **Job/**: Background job queue and runner\n- **FileStorage/**: File upload abstraction\n- **AutoRefresh.hs**: Real-time view updates via WebSocket\n- **Mail.hs**: Email composition and sending\n\n## IDE/Code Generation (`ihp-ide/`)\n\n- **exe/IHP/IDE/DevServer.hs**: Development server entry point\n- **IHP/IDE/SchemaDesigner/**: Visual database schema editor\n- **IHP/IDE/CodeGen/**: Code generators for controllers, views, migrations, jobs, etc.\n- **IHP/SchemaCompiler.hs**: Generates `Types.hs` from SQL schema\n\n## Code Guidelines\n\n- Use `pure` instead of `return` (clearer for non-Haskell developers)\n- Add Haddock comments to public APIs\n- The framework uses implicit parameters extensively (see `ImplicitParams` extension)\n- HSX uses quasiquotes: `[hsx|<div>content</div>|]`\n\n## Git Workflow\n\n- **Never force push.** If a push is rejected because the remote has advanced, `git fetch` and rebase (or merge) onto the updated remote, then push normally. Do not use `git push --force` or `--force-with-lease`.\n\n## Hasql Database Patterns (Reference)\n\nBased on patterns from [hasql-tutorial1](https://github.com/nikita-volkov/hasql-tutorial1). Hasql provides type-safe PostgreSQL access with a layered architecture.\n\n### Module Structure\n\nThe tutorial demonstrates a library-oriented structure where database code is isolated into a reusable package:\n\n```\nmy-db-library/\n├── my-db-library.cabal\n└── library/\n    └── MyDbLibrary/\n        ├── Prelude.hs      -- Re-exports (rerebase), internal\n        ├── Statement.hs    -- Raw SQL statements, internal\n        ├── Transaction.hs  -- Business logic composition, internal\n        └── Session.hs      -- PUBLIC API (only exposed module)\n```\n\n**Cabal exposure pattern:**\n```cabal\nlibrary\n  exposed-modules:\n    MyDbLibrary.Session           -- Only public module\n  other-modules:\n    MyDbLibrary.Prelude           -- Internal\n    MyDbLibrary.Statement         -- Internal\n    MyDbLibrary.Transaction       -- Internal\n```\n\n**Why this structure?**\n- **Stable API**: Only `Session` is exposed, allowing internal refactoring without breaking consumers\n- **Encapsulation**: Statements and transactions are implementation details\n- **Reusability**: The library can be shared across multiple apps (CLI, REST API, etc.)\n- **Testability**: Each layer can be tested independently\n\n### Import Pattern\n\n```haskell\n-- In Statement.hs (internal)\nmodule MyDbLibrary.Statement where\n\nimport MyDbLibrary.Prelude\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\n-- In Transaction.hs (internal)\nmodule MyDbLibrary.Transaction where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Statement as Statement\nimport qualified Hasql.Transaction as Transaction\n\n-- In Session.hs (public API)\nmodule MyDbLibrary.Session where\n\nimport MyDbLibrary.Prelude\nimport qualified MyDbLibrary.Transaction as Transaction\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n```\n\n### Layer 1: Statements (Raw SQL + Codecs)\n\nStatements combine SQL with type-safe encoders/decoders:\n\n```haskell\nimport qualified Hasql.Statement as Hasql\nimport qualified Hasql.Encoders as Encoders\nimport qualified Hasql.Decoders as Decoders\n\nfindUserByEmail :: Hasql.Statement Text (Maybe Int32)\nfindUserByEmail =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id FROM users WHERE email = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.text)\n    decoder = Decoders.rowMaybe (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ninsertUser :: Hasql.Statement (Text, ByteString, Text, Maybe Text) Int32\ninsertUser =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"INSERT INTO users (email, password, name, phone) VALUES ($1, $2, $3, $4) RETURNING id\"\n    encoder =\n      contramap (\\(a,b,c,d) -> (a,(b,(c,d)))) $\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n        Encoders.param (Encoders.nonNullable Encoders.text) <>\n        Encoders.param (Encoders.nullable Encoders.text)\n    decoder = Decoders.singleRow (Decoders.column (Decoders.nonNullable Decoders.int4))\n\ngetUserNotifications :: Hasql.Statement Int32 (Vector (Int32, Text, Bool))\ngetUserNotifications =\n  Hasql.Statement sql encoder decoder True\n  where\n    sql = \"SELECT id, message, read FROM notifications WHERE user_id = $1\"\n    encoder = Encoders.param (Encoders.nonNullable Encoders.int4)\n    decoder = Decoders.rowVector $\n      (,,) <$>\n        Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n        Decoders.column (Decoders.nonNullable Decoders.text) <*>\n        Decoders.column (Decoders.nonNullable Decoders.bool)\n```\n\nKey points:\n- `$1`, `$2` placeholders for parameters (PostgreSQL prepared statements)\n- Encoder maps Haskell tuple → PostgreSQL binary format\n- Decoder maps PostgreSQL result → Haskell type\n- Last `Bool` parameter enables statement caching\n- **Codecs stay with SQL**: Encoders/decoders defined in same module as queries\n\n### Layer 2: Transactions (Business Logic)\n\nTransactions compose statements with atomicity:\n\n```haskell\nimport qualified Hasql.Transaction as Transaction\nimport qualified Hasql.Transaction.Sessions as Sessions\n\nregister :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister email password name phone = do\n  existingId <- Transaction.statement email Statement.findUserByEmail\n  case existingId of\n    Just id -> pure (False, id)  -- User already existed\n    Nothing -> do\n      newId <- Transaction.statement (email, password, name, phone) Statement.insertUser\n      pure (True, newId)         -- New user created\n```\n\nThe `(Bool, Int32)` return type is idempotent: callers know whether the operation created a new record.\n\n**Alternative: Applicative/Selective composition** (for independent operations):\n```haskell\nimport Control.Selective (fromMaybeS)\n\nregister' :: Text -> ByteString -> Text -> Maybe Text -> Transaction (Bool, Int32)\nregister' email password name phone =\n  fromMaybeS\n    (fmap (\\newId -> (True, newId)) $\n      Transaction.statement (email, password, name, phone) Statement.insertUser)\n    (fmap (\\existingId -> (False, existingId)) $\n      Transaction.statement email Statement.findUserByEmail)\n```\n\n### Layer 3: Sessions (Public API)\n\nSessions wrap transactions and manage connections:\n\n```haskell\nimport qualified Hasql.Session as Session\nimport qualified Hasql.Transaction.Sessions as Sessions\n\n-- Using transactions (for multi-statement operations)\nregisterUser :: Text -> ByteString -> Text -> Maybe Text -> Session (Bool, Int32)\nregisterUser email password name phone =\n  Sessions.transaction Sessions.Write Sessions.Serializable $\n    Transaction.register email password name phone\n\n-- Direct statement (for single-statement operations)\nauthenticate :: Text -> ByteString -> Session (Maybe (Bool, Int32))\nauthenticate email password =\n  Session.statement (email, password) Statement.authenticateUser\n\ngetUserDetails :: Int32 -> Session (Maybe (Text, Text, Maybe Text, Bool))\ngetUserDetails userId =\n  Session.statement userId Statement.getUserDetails\n\ngetNotifications :: Int32 -> Session (Vector (Int32, Text, Bool))\ngetNotifications userId =\n  Session.statement userId Statement.getUserNotifications\n```\n\nTransaction modes:\n- `Write Serializable`: For critical operations (registration, payments) - full isolation\n- `Read ReadCommitted`: For queries - weaker isolation, better concurrency\n\n### Result Type Conventions\n\n| Pattern | Type | Use Case |\n|---------|------|----------|\n| Optional row | `Maybe a` | `SELECT` returning 0 or 1 row |\n| Multiple rows | `Vector a` | `SELECT` returning many rows |\n| Multi-column | `(a, b, c)` | Tuples, not custom wrapper types |\n| Success flag | `Bool` | `UPDATE`/`DELETE` success |\n| Idempotent | `(Bool, a)` | Indicates if operation was performed |\n\n**Why tuples over custom types?**\n- Less boilerplate\n- Results are internal to the library anyway\n- Consumers see semantic Session API, not raw tuples\n\n### Encoder Composition\n\nFor multi-parameter statements, compose encoders with `<>`:\n\n```haskell\n-- For (Text, ByteString, Int32)\nencoder =\n  contramap (\\(a,b,c) -> (a,(b,c))) $\n    Encoders.param (Encoders.nonNullable Encoders.text) <>\n    Encoders.param (Encoders.nonNullable Encoders.bytea) <>\n    Encoders.param (Encoders.nonNullable Encoders.int4)\n```\n\nThe `contramap` restructures the flat tuple into nested pairs for the `<>` combinator.\n\n### Decoder Composition\n\n```haskell\n-- Single optional row with multiple columns\nDecoders.rowMaybe $\n  (,,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Vector of rows (multiple results)\nDecoders.rowVector $\n  (,,) <$>\n    Decoders.column (Decoders.nonNullable Decoders.int4) <*>\n    Decoders.column (Decoders.nonNullable Decoders.text) <*>\n    Decoders.column (Decoders.nonNullable Decoders.bool)\n\n-- Single required row\nDecoders.singleRow $\n  Decoders.column (Decoders.nonNullable Decoders.int4)\n```\n\n### Dependencies\n\n```cabal\nbuild-depends:\n    hasql >= 1.4 && < 1.5\n  , hasql-transaction >= 0.10 && < 0.11\n  , rerebase >= 1.4 && < 1.5  -- Enhanced prelude with Text, Vector, etc.\n```\n\nNote: `hasql-transaction` is marked experimental but widely used.\n\n## Key Language Extensions\n\nThe codebase uses GHC2021 with these notable extensions:\n- `ImplicitParams`: For passing context implicitly\n- `OverloadedLabels`: For field access (`#fieldName`)\n- `QuasiQuotes`: For HSX templates\n- `TypeFamilies` and `DataKinds`: For type-safe routing and models\n\n## Faster Development Builds\n\nFor faster rebuilds during development, uncomment the `configureFlags = [ \"--flag FastBuild\" ];` line in `ihp.nix`. Don't commit this change:\n```bash\ngit update-index --assume-unchanged ihp.nix\n```\n\n## Working with a Local IHP in an App\n\nClone IHP into your app directory, then patch `flake.nix`:\n```bash\ngit clone git@github.com:digitallyinduced/ihp.git IHP\ncd IHP\nsed -i \"s|ihp.url = .*|ihp.url = \\\"path://$(pwd)\\\";|\" ../flake.nix\ndirenv allow\ncd ..\nnix flake update\n```\n\n## Documentation\n\nThe `/Guide/` directory contains comprehensive documentation in markdown. Run `devenv up` from the Guide directory to preview changes locally.\n\nThe production version of the documentation is hosted at: ihp.digitallyinduced.com\n\nTo update it:\n- `cd ~/digitallyinduced/ihp-website && nix flake update ihpDocs`. Then commit the lock file change\n- Next deploy the nixos configuration: `cd ~/digitallyinduced/servers && nix flake update ihpWebsite && nix run .#deploy`\n\nWhen a new version of IHP is released, also update the ihp-website repo\n","category":"root","tokens":3485}]}