Open-source project
obsidiansystems/obelisk avatar
obsidiansystems/obelisk

Obelisk packages a Reflex web app with its Nix toolchain and deployment targets

Functional reactive web and mobile applications, with batteries included.

1,041 stars115 forksHaskellNOASSERTION

At a glance

What is it?
Obelisk is a Haskell framework from Obsidian Systems that scaffolds a Reflex or Reflex-DOM application with a shared common layer, a Snap backend and a WASM or GHCJS frontend, and ships the build toolchain as a Nix shell so the same codebase deploys to a NixOS server, an OCI container or a Capacitor mobile app.
Who is it for?
Obelisk fits a Haskell team that wants Reflex with the wiring already decided, the shared types already spanning the frontend and backend boundary, and a deployment story that covers a NixOS server, a container and a mobile app from one codebase.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly Haskell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • obsidiansystems/obelisk1,041 ★ · +4
Star history
Repository2026-10-212026-11-07
obsidiansystems/obelisk1k1k

4 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

One codebase, one type system, three deployment targets

Obelisk describes itself as functional reactive web and mobile applications, with batteries included, and its goal is to represent a cohesive, highly-curated set of choices that Obsidian Systems has made for building these types of applications in a way that is extremely fast but does not compromise on production readiness.

The choices are specific. It supports both WASM and GHCJS frontend targets, GHC 9.14, and deploys to NixOS servers or OCI containers.

So the curating is of the whole stack, not just the framework layer. A Reflex application needs a backend, a routing story, a build pipeline for the frontend, a static asset pipeline, and a way to ship the result, and each of those is a decision. Obelisk makes them.

The payoff is stated in the Overview. In minutes you can go from an empty directory to an interactive application running in the browser, all from a single Haskell codebase, and the same frontend assets can be packaged as a mobile app with CapacitorJS.

The part that matters most for a Haskell team is the boundary argument. You can take advantage of Haskell's type system across the frontend and backend boundary, which means changes to your backend that would break your frontend are immediately detected during development and vice versa, and Obelisk uses Haskell's compiler to give you a complete TODO list of what needs to be updated.

That is the strongest argument for the framework, and it is a direct consequence of the common/ directory being shared. A route defined once, with its types, is available to the server that serves it and the client that links to it, and if the two disagree the compiler says so rather than a user finding a broken link.

The audience is stated plainly. Obelisk is targeted primarily at Haskell developers who want to build high-quality web applications in Haskell, without the distractions of manually choosing and integrating technology for every piece of the system.

The project structure shows the shared layer

The project structure listing is the clearest description of the architecture, and one directory does most of the work.

code
my-app/
  backend/             # Snap backend server
  common/              # Shared types and routes
  config/              # Runtime config (common/, frontend/, backend/)
  frontend/            # Reflex-DOM frontend (library + executable)
    js/                # GHCJS cross-compilation wrapper
    wasm/              # WASM cross-compilation wrapper
  static/              # Static assets (css, images, etc.)
  deps/
    obelisk/           # obelisk itself, as a git submodule
    nix-haskell        # Symlink to obelisk/deps/nix-haskell
    nixpkgs            # Symlink to nix-haskell/pins/nixpkgs
    reflex-dom         # Symlink to obelisk/deps/reflex-dom
  cabal.project        # Cabal project file
  project.nix          # Nix-haskell project configuration
  release.nix          # Release matrix (every driver and frontend target)

The common/ directory is the one that carries the type safety argument, shared types and routes. The backend is a Snap server, the frontend is a Reflex-DOM library plus an executable, and the config/ directory holds runtime configuration split three ways, common, frontend and backend, so a value can be defined once and consumed at the layers that need it.

The frontend has two cross-compilation wrappers, js/ for GHCJS and wasm/ for WASM, which is how one frontend codebase targets both. The static/ directory has a generate script and a generated/ subdirectory holding the generated static module, obelisk-generated-static, with a custom/ subdirectory for a custom Setup.hs, so the static asset generation is part of the normal Haskell build rather than a separate tool.

The deps/ directory is where the pinning story lives. obelisk itself is a git submodule, and nix-haskell, nixpkgs and reflex-dom are symlinks into it, which means the dependency set is whatever the pinned revision carries rather than something the project resolves on its own.

Scaffolding, the dev server, and the submodules

The quick start shows how little you have to do, and the caveat about submodules is the part that trips people up.

bash
nix run github:obsidiansystems/obelisk#init -- my-app
# Nix < 2.27: nix run 'github:obsidiansystems/obelisk?submodules=1#init' -- my-app
cd my-app
nix-shell -A haskell-nix
ob-run

The prerequisites are stated before that. Nix with the reflex-frp binary caches configured, and without the caches your first build compiles the toolchain from source, which is a long build.

The submodule detail is explained in the README in a way that suggests it has caused trouble. The project carries obelisk as a git submodule at deps/obelisk, pinned to the revision it was scaffolded from, with obelisk's own submodules checked out, and you bump the pin with three git commands.

bash
git -C deps/obelisk fetch
git -C deps/obelisk checkout <rev>
git -C deps/obelisk submodule update --init --recursive

And because every build reads that submodule, you must clone with --recurse-submodules or run git submodule update --init --recursive afterwards, and on Nix older than 2.27 you also add ?submodules=1 to the flake URL.

The README also suggests two global git settings, submodule.recurse true to keep deps/ at the pinned revision across clone, pull and checkout, and diff.submodule log so a pin bump shows as the commits it moves across. The second one is a nice touch, because a submodule bump is otherwise invisible in a diff.

From an existing obelisk checkout you run nix-shell -A haskell-nix and then ob-init my-app, and ob-init --help explains pinning versus linking.

The development loop is the ob-run command, a watch-and-rebuild development server that rebuilds the backend on .hs, .cabal or .project file changes or when you press Enter, disables optimizations with -O0 for fast rebuilds, and cross-compiles the frontend in the background during each build through the backend's custom Setup.hs. The server listens on port 8000 by default, and arguments after a double dash go to cabal run and then to the backend.

Nix is optional day to day and required for production

The Who should consider using it section is where the prerequisites are set out with unusual care, and it is worth quoting because it describes the actual cost of the framework.

Obelisk assumes basic knowledge of Haskell and Reflex/Reflex-DOM, web technologies like HTML and CSS, and a terminal shell like Bash. Knowledge of Nix helps but is not required for day-to-day development: once the cross-toolchain is on your PATH, the edit loop is plain cabal. Production builds and the shipped static-asset pipeline do use nix.

That is a useful and unusual distinction. A lot of Nix-based Haskell projects make the toolchain inescapable, and here the argument is that the daily loop is cabal, with Nix as the packaging layer. The edit loop being plain cabal is what makes the type system across the frontend and backend boundary practical, since the rebuild has to be fast.

The development commands reflect that split. There is a haskell-nix shell attribute, or nix develop, which gets you all the build tools including GHC, cabal, the cross-compilers and hoogle. And the release.nix file is described as the release matrix, every driver and frontend target, which is the file that turns the same source into a server deployment, a container and a mobile bundle.

The build scripts in the top level listing back this up, with nix/ for the Nix expressions, lib/ for the library code, skeleton/ for the project template, scripts/ for the tooling, docs/ for the documentation, guides/ for the longer guides, thunk.nix for the dependency pinning, flake.nix and flake.lock for the flake, default.nix, shell.nix and inputs.nix for the Nix entry points, and the deps/ directory for the pinned sources.

The project carries FAQ.md, CONTRIBUTING.md and a ChangeLog.md at the top level, and the release history shows a jump from v1.3.0.0 in December 2023 to v2.0.0.0 on 2026-10-10, with the last push on 2026-10-15.

Where Obelisk is the wrong choice

The first wrong case is a team that does not already know Haskell and Reflex. The README is explicit that Obelisk is targeted primarily at Haskell developers, and the assumption list names Haskell, Reflex and Reflex-DOM, HTML, CSS and Bash as baseline knowledge. There is no layer here that makes a frontend framework approachable to someone who has not used one.

The second wrong case is a project that wants to choose its own stack. A framework whose value is that it has made the choices for you is a framework that is hard to use if you want different choices. If you need a different database, a different routing library or a different frontend rendering approach, you are undoing the framework rather than using it.

The third wrong case is a team without a tolerance for toolchain complexity. The everyday loop is cabal, which is friendly, but the production path is Nix, the first build needs the reflex-frp binary caches or it compiles the toolchain from source, the project carries obelisk as a pinned git submodule with its own submodules, and the release matrix is a Nix expression covering every driver and frontend target. A team that would rather not maintain that has a lot of operational surface to absorb.

The fourth wrong case is a team that wants a small, quick change to a web application. A Reflex application with a shared type system across the boundary and a WASM or GHCJS frontend is a substantial build, and the frontend is cross-compiled during each development build, which is the cost of the type safety the framework buys.

How it differs from writing a Reflex application by hand

The comparison worth making is with a Reflex or Reflex-DOM application assembled from the parts yourself, and the difference in approach is the number of decisions you do not have to make.

Doing it by hand means choosing a backend and wiring the routing so that the client-side routes and the server-side routes agree, and setting up the cross-compilation for the frontend so the browser gets a JavaScript or WASM build of your Reflex-DOM code, and building a static asset pipeline for the CSS and images, and then figuring out how to ship all of that. Each of those is a day or two of work, and each of them is a decision that will be made differently in every project.

Obelisk's contribution is the standardisation and the shared layer. The common/ directory with shared types and routes is the part that changes the character of the work, because the type system now spans the boundary and the compiler reports what breaks on the other side. That is a different development experience from two separately typed halves that meet at an HTTP contract.

The second difference is the deployment matrix. One codebase producing a NixOS server deployment, an OCI container and a Capacitor mobile app is a substantial amount of packaging work, and it is the kind of thing a framework does better than a team because it has to be right for every combination of driver and frontend target.

The third difference is the template. The skeleton/ directory is the project the ob-init command copies, and because the template is maintained by the framework's authors, a new project starts with a working, tested configuration rather than one assembled from a blog post.

The fourth difference is the cost. The framework asks for Nix, a pinned submodule and a cross-compiled frontend, and it asks for a team that already works in Haskell. That is a high entry price in exchange for not making the decisions, and the exchange is only worth it if the decisions are not what you want to make.

Editorial conclusion

Obelisk fits a Haskell team that wants Reflex with the wiring already decided, the shared types already spanning the frontend and backend boundary, and a deployment story that covers a NixOS server, a container and a mobile app from one codebase. It does not fit a team that does not already know Haskell and Reflex, since the README states it is targeted primarily at Haskell developers, and it does not fit a project that needs to choose its own database, its own routing and its own frontend framework. Before adopting it, install Nix with the reflex-frp binary caches configured as the quick start requires, and note that the newest release is v2.0.0.0 from 2026-10-10 with the last push on 2026-10-15, that the licence is BSD3 as the badge says, and that production builds and the static asset pipeline do use Nix even though day-to-day development does not.

Frequently asked questions

What is Obelisk and who is it for?

Obelisk is a Haskell framework from Obsidian Systems for building functional reactive web and mobile applications with batteries included, using Reflex. It is targeted primarily at Haskell developers who want to build high-quality web applications in Haskell without manually choosing and integrating technology for every piece of the system.

How do I start a new Obelisk project?

Run nix run github:obsidiansystems/obelisk#init -- my-app, or with ?submodules=1 on Nix older than 2.27, then cd into the directory, run nix-shell -A haskell-nix and ob-run, and open http://localhost:8000. From an obelisk checkout you use ob-init my-app instead.

What does the common/ directory do in an Obelisk project?

It holds the shared types and routes used by both the backend and the frontend. Because it is compiled as part of one Haskell codebase, changes to the backend that would break the frontend are detected by the compiler during development, which the README describes as a complete TODO list of what needs to be updated.

What deployment targets does Obelisk support?

It supports both WASM and GHCJS frontend targets with GHC 9.14, and deploys to NixOS servers or OCI containers, with the same frontend assets packageable as a native mobile app through CapacitorJS. The release.nix file is the release matrix covering every driver and frontend target.

Do I need Nix to develop with Obelisk?

Not for day-to-day work. The README says knowledge of Nix helps but is not required for day-to-day development, because once the cross-toolchain is on your PATH the edit loop is plain cabal. Production builds and the shipped static-asset pipeline do use Nix, and the quick start requires Nix with the reflex-frp binary caches configured.

Official sources

  1. Issues
  2. obsidiansystems/obelisk on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/obsidiansystems-obelisk.svg)](https://hysenlabs.com/projects/obsidiansystems-obelisk)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/obsidiansystems-obelisk.svg)](https://hysenlabs.com/projects/obsidiansystems-obelisk)

Was this useful?