Wisp is an open-source, self-hostable blogging and membership platform. It is licensed AGPL-3.0, single-site-per-install (you clone, configure, and run your own instance), and built to be a modern, fast alternative to WordPress and Ghost.
The stack
Wisp is type-first: warnings_as_errors is enforced, every public function carries
an @spec, and the Boundary library checks the
architecture at compile time. The stack is:
- Elixir 1.20 + Erlang/OTP 28 as the language and runtime
- Phoenix 1.8 + LiveView 1.1 for the web layer (live-preview editor, real-time comments, presence, claps) on the Bandit HTTP server
- SQLite (WAL mode, single-writer) via
ecto_sqlite3, with Litestream continuous backups to S3-compatible object storage - MDEx (comrak NIF) for Markdown rendering, SQLite FTS5 for search, Oban Lite for background jobs
Headless core + ports
Wisp follows a 3-ring boundary model:
- Domain core (
lib/wisp/) — type-first contexts with zero Phoenix/Plug/LiveView dependencies: Content, Members, Staff, Comments, Search, Analytics, and more. - Ports — config-injected adapters that live OUTSIDE the core:
Wisp.Billing,Wisp.Mailer,Wisp.MediaStorage, andWisp.ThemeEngine. - Delivery layer (
lib/wisp_web/) — the admin write surface, the public read/theme render, and the Ghost Content API v5; this ring may not call Ecto directly.
Every major integration is a pluggable port behaviour. Swapping storage from local disk to S3, or the theme engine, is a one-line config change:
config :wisp, :media_adapter, Wisp.MediaStorage.S3
The content-gating model
Wisp's gating invariant is enforced once, in Wisp.ReadModel, by projecting a single
canonical source row through two distinct functions:
theme_projection/2powers the site/theme channel; a gated post shows a fixed paywall CTA with no body, no teaser snippet, and no heading-structure leak.content_api_projection/2powers the Ghost API channel; a gated post returnsaccess: false,html: null, and zero body bytes.
Posts have three visibility tiers — public, members, and paid — and the gate is the same on every surface (feeds, search snippets, related posts, and the sitemap all exclude gated bodies). A teaser can never leak a members-only body.
Realtime, off the hot path
Wisp ships realtime features that stay off the SQLite write path by design:
- An admin Pulse dashboard with a live MRR / subscriber ticker and newsletter send-progress bar
- Reader presence ("N reading now" + face-pile avatars)
- Live claps via in-memory
:counters, flushed to the database by an Oban cron - Buffered analytics (a GenServer + ETS counter, batched upsert via Oban)
Why Wisp over Ghost or WordPress?
- True self-host: no Ghost(Pro) tax, no external image service, no managed infra
- Simpler ops: one SQLite file with Litestream backups, no Postgres to run
- Type-first from commit 1: the compiler prevents whole classes of bugs, including accidental gated-content leaks
- Headless core + ports: swap Stripe for another processor, or disk for S3, in config
- Realtime out of the box: LiveView comments/claps/presence with no REST polling
- No PHP, no plugin security debt (vs. WordPress); Markdown authoring, not WYSIWYG lock-in
Honest positioning
Wisp is a young project built to Phase-1 completeness with 34 WordPress-gap features shipped. A few things are deliberately on the roadmap rather than done:
- The Ghost-Handlebars theme engine is roadmap, not shipping — HEEx is the default and only production theme engine today.
- The Wax (passkey) auth is unaudited; a mandatory magic-link fallback ensures staff can always sign in.
- SQLite's single-writer ceiling is real (~50 emails/min sustained); the hot-path work is buffered off it, and Postgres is the documented escape hatch above that.
Read the rest of this series for deploying to Fly.io, the paid-membership story, configuration, and a tour of the admin.