## https://sploitus.com/exploit?id=E6CA565D-F8FF-5FE6-8418-330C2C837786
# Wisp โ the open-source Ghost alternative, built in Elixir & Phoenix LiveView
**A type-first, self-hosted blog & membership platform.** Elixir 1.20, Phoenix LiveView,
and a single SQLite file. No Postgres to run, no PHP, no plugin security debt โ just a
fast, modern, AGPL-licensed publishing platform you own end to end.
[](https://elixir-lang.org)
[](https://www.erlang.org)
[](https://www.phoenixframework.org)
[](LICENSE)
[](#testing)
> **Honest status:** Wisp is a young, Phase-1-complete project. It is **ready as a free,
> self-hosted blog + newsletter** today. The **billing adapter is a stub** โ you must
> replace it before charging money. It is **single-node SQLite with no high availability**.
> See [Production readiness](docs/PRODUCTION_READINESS.md) for the full, un-inflated rating.
---
## What is Wisp?
Wisp is an **open-source, self-hostable blogging and membership platform** โ a modern
alternative to **Ghost** and **WordPress**, written in **Elixir**. You clone it, configure
it, and run your own single-site instance. It ships content publishing, membership tiers
(free/paid), newsletters, real-time comments, privacy-first analytics, and a Ghost-compatible
Content API โ backed by **SQLite** and deployable to a single Fly.io machine with continuous
backups.
### Why Wisp?
- **True self-host.** No Ghost(Pro) tax, no external image service, no managed infra.
- **Simpler ops.** One SQLite file (WAL mode) with Litestream backups โ no Postgres to run.
- **Type-first from commit 1.** `warnings_as_errors`, `@spec` on the public surface, and
compile-time architectural enforcement prevent whole classes of bugs โ including
accidental gated-content leaks.
- **Headless core + pluggable ports.** Swap Stripe for another processor, or disk for S3,
with a one-line config change.
- **Realtime out of the box.** LiveView comments, claps, reader presence, and a live admin
dashboard โ no REST polling.
- **No PHP, no plugin security debt.** Markdown authoring, not WYSIWYG/plugin lock-in.
---
## Features
**Content & publishing:** posts (draft โ scheduled โ published), pages, categories, tags,
multiple co-authors, featured images + alt text, sticky posts, password-protected posts,
scheduled publish (Oban cron), post revisions + history, trash + restore, post duplication,
multi-part series, related posts.
**Visibility & gating:** public / members-only / paid-only tiers, dual-projection read
model (`theme_projection` for HTML, `content_api_projection` for the API), password
protection, **gating enforced at the query layer, not the view layer**.
**Editor & authoring:** LiveView live-preview editor, Markdown source authoring, two render
trust profiles (trusted for authors, untrusted for comments), syntax highlighting, sanitized
custom-CSS injection, per-post SEO overrides, reading time, table of contents, HTML cache
on publish.
**Navigation & discovery:** custom primary menu with theme-default fallback, breadcrumb /
series navigation, dashboard stats, and **full-text search (SQLite FTS5 with bm25 ranking)**.
**SEO & feeds:** `sitemap.xml`, operator-customizable `robots.txt`, RSS / Atom / JSON feeds
(Ghost Content-API compatible, scopeable by category/tag/section), canonical URLs, OpenGraph
+ Twitter card meta, site-wide SEO defaults + per-post override.
**Membership & billing:** free + paid tiers, magic-link member auth, WebAuthn passkey staff
auth with mandatory magic-link fallback, token-based member sessions, newsletter status per
member, opaque unsubscribe tokens, provider-agnostic subscriptions table, webhook
idempotency, entitlement checks (`can_view?/2`). **(Billing processor is a stub โ see below.)**
**Newsletters:** issue composition from posts, per-recipient untrusted-HTML rendering,
RFC-8058 one-click `List-Unsubscribe` headers, a sliding-window throttle GenServer,
Oban-job-based send, and bounce/complaint delivery tracking.
**Comments:** member comments in real time (LiveView), threaded tree, Markdown authoring
with a tight untrusted allowlist (`nofollow` links, no inline HTML), a moderation state
machine (`auto_approve` / `hold` / `spam`), PubSub fan-out, optional email notifications,
spam detection, and per-member rate limiting.
**Analytics (privacy-first):** cookieless pageview tracking, an in-process buffered counter
(GenServer + ETS) batch-flushed to SQLite via Oban, daily rollups, bot filtering, and
realtime gauges (MRR, claps).
**Media:** upload + storage with a **local-disk default** and a **pluggable S3 adapter**,
alt text per image, and an admin media library with a conservative orphan-cleanup job.
**Appearance:** accent-color picker (strict hex), heading-font selector (curated allowlist),
sanitized custom-CSS injection, theme picker (HEEx default), settings singleton.
**Admin & moderation:** staff login (passkeys + fallback), roles (owner / admin / author /
contributor), comment moderation queue, **audit log (append-only diffs)**, bulk actions,
a first-run setup wizard, and lockout-safe maintenance mode.
**Import / export:** manager-only JSON backup (all content + settings, **excludes members /
PII**) and idempotent upsert-by-slug import.
**API (Ghost-compatible):** Content API (`GET /content/posts/:slug`) and a token-auth Admin
API, in the Ghost Content API v5 field shape, with gating-correct projections.
For the complete inventory and the load-bearing architectural invariants, see the
[guides](docs/guides/getting-started.md).
---
## Tech stack & architecture
| Layer | Choice |
|-------|--------|
| Language / runtime | **Elixir 1.20 / Erlang-OTP 28** |
| Web | **Phoenix 1.8 + LiveView 1.1** on **Bandit** |
| Database | **SQLite** (WAL mode, single-writer) via `ecto_sqlite3` |
| Backups | **Litestream** โ S3-compatible object storage |
| Jobs / cron | **Oban Lite** |
| Email | **Swoosh** (SMTP + transactional) |
| Markdown | **MDEx** (comrak NIF) |
| Search | **SQLite FTS5** (bm25) |
| Passkeys | **Wax** (WebAuthn) |
| Styling | **Tailwind CSS + daisyUI** |
| Architecture | **Boundary** (compile-time layering) |
### Three-ring design (compile-enforced)
```
Ring 3 WispWeb.Admin / WispWeb.Site / WispWeb.API delivery โ NO Ecto
Ring 2 Billing ยท Mailer ยท MediaStorage ยท ThemeEngine ports โ pluggable adapters
Ring 1 Wisp (core) domain โ ZERO Phoenix/LiveView deps
```
- **Boundary** enforces these rings at **compile time** โ the web layer cannot import Ecto
and the core has zero Phoenix deps. Violating the layering **fails the build**, not review.
- **ReadModel gating** is the load-bearing invariant: one canonical source row projects
through `theme_projection/2` (teaser/paywall) and `content_api_projection/2`
(`access: false`, `html: null`, zero body bytes). The gate is enforced **exactly once**.
- **Ports are pluggable behaviours.** `Wisp.Billing`, `Wisp.Mailer`, `Wisp.MediaStorage`,
and `Wisp.ThemeEngine` are config-injected adapters living outside the core.
---
## Quick start (local development)
You need **Elixir 1.20 / OTP 28**. The repo pins versions in `.tool-versions`, so
[asdf](https://asdf-vm.com) or [mise](https://mise.jdx.dev) will install them automatically.
```bash
git clone https://github.com/Evoke4350/wisp.git
cd wisp
mix setup # deps.get + ecto.setup (create/migrate/seed) + asset build
mix phx.server # http://localhost:4000 (or: iex -S mix phx.server)
```
On first run, open [`/admin/setup`](http://localhost:4000/admin/setup) and walk the wizard
(name your site, create the owner staff user, enroll a passkey, connect a recovery path).
Full walkthrough in the [Getting started guide](docs/guides/getting-started.md).
---
## Deploy to Fly.io
Wisp deploys as a **single stateful machine**: an OTP release with **SQLite on a mounted
volume** and **Litestream** streaming the WAL to object storage. The full copy-pasteable
sequence (app + volume, runtime secrets, custom domain, boot-time migrations) is in the
**[Deploying to Fly.io guide](docs/guides/deploying.md)**.
---
## Configuration
Wisp config lives in clearly separated layers: compile-time (`config/*.exs`), runtime
(`config/runtime.exs` + env vars / Fly secrets), and a database-backed `Settings` singleton
editable from the admin UI. Every knob and where to set it is in the
**[Configuration guide](docs/guides/configuration.md)**.
---
## Pros and cons
Honest summary (full version: [Pros and cons](docs/PROS_AND_CONS.md)).
**Pros**
- Type-first: `warnings_as_errors`, ~616 `@spec`/`@type` declarations, compile-time Boundary
enforcement.
- Content gating enforced once and **canary-tested** to never leak a gated body.
- Strong security: hashed single-use member tokens, passkeys + mandatory fallback, strict
XSS defense, append-only audit log.
- Simple, cheap ops: one SQLite file + Litestream backups, boot-time migrations.
- Realtime (comments / claps / presence / Pulse) kept off the SQLite write path.
- Headless core + pluggable ports (billing, mailer, media, theme).
- 1,527 tests, run serially, **zero skipped / zero flaky**.
**Cons**
- **Billing is a STUB** โ no real processor, no pricing UI, no signature verification. You
must replace it before charging.
- **No subscription renewal logic** โ paid members never expire today.
- **No member-management UI**, no membership emails; MRR gauge does not persist (zeros on
restart).
- **Single-node SQLite, single-writer.** ~50 emails/min ceiling. **No HA / clustering /
failover.** Postgres is the documented escape hatch.
- **Wax passkey auth is unaudited** (mitigated by the mandatory magic-link fallback).
- Litestream backup can fail **silently** if credentials are missing.
- No server-side thumbnail resizing; Ghost-Handlebars theme engine is roadmap (HEEx only).
---
## Production readiness
**Overall: 7.3 / 10** โ a solid Phase-1 foundation with strong security and architecture;
billing is the only showstopper, and it only blocks paid use cases.
| Use case | Rating | Verdict |
|----------|:------:|---------|
| Solo / small self-hosted blog + newsletter (minimal monetization) | **8.5 / 10** | **READY** |
| Payment-critical SaaS (Stripe / LemonSqueezy) | **4 / 10** | **NOT READY** โ billing ring is a no-op |
| High-write-concurrency app (> 50 emails/min sustained) | **3 / 10** | **NOT READY** โ SQLite single-writer; Postgres is the escape hatch |
**Load-bearing caveats** (the un-inflated ones):
- The **stub billing adapter is the only processor today** โ it is not a fallback. Replacing
it (plus webhook signature verification, a pricing UI, and renewal/reconciliation logic) is
a hard requirement before accepting payment.
- **Single node, no high availability.** SQLite is single-writer and the realtime tier is
single-node. Durability comes from Litestream point-in-time recovery, **not redundancy**.
- **Wax passkey auth is unaudited;** the mandatory magic-link fallback ensures staff can
always sign in.
- **Litestream credentials are runtime secrets** โ if missing, Litestream starts with no
backup (silent failure). Verify after first deploy.
- **`PHX_HOST` is critical** โ misconfiguration breaks magic-link URLs and the WebAuthn
`rp_id` derivation.
- The **test suite runs serially** (`max_cases: 1`) โ correct and intentional for SQLite's
single writer, not a weakness.
Full per-use-case breakdown and the shipping checklist: [Production readiness](docs/PRODUCTION_READINESS.md).
---
## Roadmap / deferred
Deliberately on the roadmap, not done (none of these block a free solo blog):
- **Real billing adapter** + pricing UI + subscription renewal/reconciliation + billing
emails + member-management UI (cancel/upgrade/dispute) + persisting the MRR gauge.
- **Ghost-Handlebars theme engine** โ a separate R&D track behind the `ThemeEngine` port;
HEEx is the shipped default.
- **Server-side thumbnail resizing** โ featured images are currently stored raw.
- **Postgres escape hatch** โ documented as the path above the SQLite single-writer ceiling.
---
## Testing
```bash
# Serial run (deterministic SQLite single-writer behavior), warnings as errors:
mix test --warnings-as-errors --max-cases 1
```
1,527 test cases across 140 files. Serial execution is enforced via `max_cases: 1` in
`test_helper.exs`. There are **zero `@tag :skip`** and **zero `@tag :flaky`** tests.
---
## Contributing
Contributions are welcome. Please read **[CONTRIBUTING.md](CONTRIBUTING.md)** for the dev
setup, the green-gate (`mix compile --warnings-as-errors` + the serial test run), the
type-first discipline, and the Conventional Commits convention. By participating you agree
to the [Code of Conduct](CODE_OF_CONDUCT.md). Security issues: see
**[SECURITY.md](SECURITY.md)** (responsible disclosure + the content-gating no-leak invariant).
---
## License
Wisp is licensed under the **GNU Affero General Public License v3.0 or later**
(`AGPL-3.0-or-later`). See [LICENSE](LICENSE).
```
SPDX-License-Identifier: AGPL-3.0-or-later
```
> **Dependency note:** [Oban](https://github.com/oban-bg/oban) is licensed under the Business
> Source License 1.1 (which converts to Apache-2.0 after a sunset period). It is not a
> copyleft conflict with AGPL-3.0, but commercial users should review Oban's license terms.
> All other runtime dependencies are Apache-2.0, MIT, or CC0.