Thesis

What is Wisp?

3 min read
Guides

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:

  1. Domain core (lib/wisp/) — type-first contexts with zero Phoenix/Plug/LiveView dependencies: Content, Members, Staff, Comments, Search, Analytics, and more.
  2. Ports — config-injected adapters that live OUTSIDE the core: Wisp.Billing, Wisp.Mailer, Wisp.MediaStorage, and Wisp.ThemeEngine.
  3. 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/2 powers 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/2 powers the Ghost API channel; a gated post returns access: 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.