An opinionated starter template for event-sourced NeoHaskell applications. Clone it, rename the Counter domain to whatever you're building, and you're off.
- Event-sourced CQRS skeleton with a working Counter example — 3 events, 3 commands, 1 read model.
- Nix-managed toolchain: GHC, Cabal, HLS, fourmolu, hlint, hurl — pinned and reproducible.
- In-memory event store by default (zero setup). PostgreSQL is fully scaffolded and one uncomment away.
- HTTP transport on
:8080with auto-generated routes (POST /commands/kebab-case-name). - Hurl acceptance tests demonstrating the full create → increment → query flow.
- AI onramp:
AGENTS.md+.skills/so coding assistants pick up the conventions immediately.
- Determinate Nix — the only thing you install yourself.
- direnv (optional) — auto-loads the Nix shell when you
cdinto the directory.
Everything else (GHC, Cabal, HLS, fourmolu, hlint, hurl) comes from the Nix shell.
# Clone and enter
git clone <your-fork-url> my-app && cd my-app
# Enter the dev shell. --accept-flake-config opts into the IOG + NeoHaskell
# binary caches (see "Binary caches" below) so GHC + deps are downloaded,
# not compiled. Takes a few minutes first time; seconds afterwards.
nix develop --accept-flake-config
# Build and run
cabal build
cabal run neohaskell-starterIn another terminal:
# Create a counter
curl -X POST http://localhost:8080/commands/create-counter \
-H 'Content-Type: application/json' \
-d '{"label": "downloads"}'
# → {"entityId": "…"}
# Increment it
curl -X POST http://localhost:8080/commands/increment-counter \
-H 'Content-Type: application/json' \
-d '{"entityId": "PASTE-UUID", "amount": 3}'
# Read the projection
curl http://localhost:8080/queries/counter-viewOr run the scripted end-to-end flow:
hurl tests/scenarios/counter-flow.hurlBuilding GHC + haskell.nix from source takes hours. flake.nix declares two public binary caches that ship prebuilt artifacts:
| Cache | Contents |
|---|---|
https://cache.iog.io |
haskell.nix / IOG artifacts (GHC, libraries) |
https://neohaskell.cachix.org |
NeoHaskell core + integrations |
Nix will only use these if you opt in. Pick one:
Option A — per command (zero config). Pass the flag on every nix develop:
nix develop --accept-flake-configOption B — accept once, forever. Add accept-flake-config = true to your user Nix config:
mkdir -p ~/.config/nix
echo 'accept-flake-config = true' >> ~/.config/nix/nix.confAfter this, nix develop (no flag) transparently uses the caches for this flake and any other that declares nixConfig.extra-substituters. If you'd rather scope it tightly, you can also add yourself to trusted-users in /etc/nix/nix.conf instead — see the NeoHaskell installation guide.
If you skip both, Nix prompts you the first time and falls back to building from source if you decline. Expect a very long first build.
.
├── launcher/Launcher.hs # Thin entry — buffers stdout/stderr, runs App
├── src/
│ ├── App.hs # Application wiring (event store, transports, services)
│ ├── Starter/Config.hs # Typed config (port, upload dir, optional Postgres)
│ └── Starter/Counter/ # Example bounded context — rename or delete
│ ├── Core.hs # Re-exports Entity and Event
│ ├── Entity.hs # CounterEntity + update function
│ ├── Event.hs # Sum type of all counter events
│ ├── Service.hs # Registers commands with the framework
│ ├── Events/ # One file per event type
│ ├── Commands/ # One file per command type
│ └── Queries/ # One file per read model
├── tests/
│ ├── integration/smoke.hurl # Server-is-up check
│ └── scenarios/counter-flow.hurl # End-to-end workflow
├── .skills/ # AI assistant knowledge bases (domain-neutral)
├── AGENTS.md # Patterns + conventions for humans and AIs
├── cabal.project # Pins NeoHaskell at a specific commit
├── flake.nix # Nix shell definition
├── docker-compose.yml # Postgres — only when you switch event stores
└── neohaskell-starter.cabal # Package definition + extensions
The starter's namespace is Starter.Counter.*. When you're building "Books" for a "Library" app, follow this 4-step checklist:
- Rename the directory.
mv src/Starter src/Library, thenmv src/Library/Counter src/Library/Book. - Fix the module names in every
.hsfile undersrc/Library/Book/— changemodule Starter.Counter.Xtomodule Library.Book.Xand everyimport Starter.Counter.Xtoimport Library.Book.X. - Update
neohaskell-starter.cabal— rewrite theexposed-moduleslist, and changeStarter.ConfigtoLibrary.Config. Optionally rename the package itself (name: library,executable library). - Update
src/App.hs— changeStarter.*imports toLibrary.*.
Then:
cabal buildIf it compiles, you're done.
Say you want to record when a counter is reset to zero. Four steps:
-
Create the event file
src/Starter/Counter/Events/CounterReset.hs:module Starter.Counter.Events.CounterReset (Event (..)) where import Core import Json qualified data Event = Event { entityId :: Uuid } deriving (Generic, Show) instance Json.FromJSON Event instance Json.ToJSON Event
-
Wire it into the sum type in
src/Starter/Counter/Event.hs— add aCounterResetvariant and a branch ingetEventEntityId. -
Handle it in
updateinsrc/Starter/Counter/Entity.hs:CounterReset _ -> entity { value = 0 }
-
Expose the module — add
Starter.Counter.Events.CounterResettoexposed-modulesin the.cabalfile, thencabal build.
(Add a matching Commands/ResetCounter.hs and register it in Service.hs if you want an HTTP endpoint too.)
The starter uses an in-memory event store so it runs out of the box. When you're ready for durability:
- Open
src/App.hsand follow theSWITCH TO POSTGREScomment block. - Open
src/Starter/Config.hsand uncomment the five Postgres fields. - Copy
.env.exampleto.env(the defaults matchdocker-compose.yml). - Start Postgres:
docker compose up -d. - Rebuild:
cabal build. The event-store schema is created automatically on first run.
# Start the server in one terminal
cabal run neohaskell-starter
# Run tests in another
hurl tests/integration/smoke.hurl
hurl tests/scenarios/counter-flow.hurlWrite new tests as .hurl files under tests/commands/ (single-command) or tests/scenarios/ (multi-step). Use [Options] retry: 10 retry-interval: 200 on GETs to wait for projections.
Inside the Nix shell:
fourmolu -i src/ launcher/ # Format
hlint src/ launcher/ # LintAGENTS.md at the root spells out the NeoHaskell conventions this project enforces — share it with any AI assistant you use (Claude Code, Codex, Cursor, etc.). Deeper reference material lives under .skills/.
- NeoHaskell docs: https://neohaskell.org
- Event Modeling: https://eventmodeling.org
- Hurl (tests): https://hurl.dev