Long-form writing on nostr. Read, highlight, write and publish articles that live on relays instead of on someone's server — and get paid for them in bitcoin over lightning.
Habla is a static single-page app. There is no backend: the browser talks to nostr relays directly, signs with the reader's own key, and stores drafts locally. The whole thing is a folder of files you can host anywhere, including on nostr itself as an nsite.
- Read long-form articles (NIP-23, kind
30023) from any relay, by author, by hashtag, or by relay. Everything is addressed by nostr identifier —/a/<naddr>for an article,/p/<nprofile>for an author — so no copy of Habla depends on owning a domain. - Write and publish with a rich-text editor that round-trips Markdown, with
drafts saved to
localStorageand images uploaded to Blossom servers. - Highlight passages (kind
9802) and comment on them (NIP-22). - Zap authors over lightning, with a built-in NWC wallet.
Requires Node 20+ and pnpm.
pnpm install
pnpm dev # http://localhost:5173Other scripts:
| Command | What it does |
|---|---|
pnpm build |
Builds to build/client |
pnpm preview |
Serves the built site with SPA fallback on :4173 |
pnpm typecheck |
Route typegen + tsc |
pnpm test |
Vitest |
pnpm deploy:nsite |
Builds and publishes to nostr with nsyte |
There are no environment variables.
pnpm build produces build/client: plain HTML, JS, CSS and assets. Serve it
from anything, with one requirement — unknown paths must fall back to
/index.html, or a link like /a/naddr1... will 404 instead of booting the
app.
nsyte uploads the build to Blossom
servers and publishes the file map to relays, so the site is addressed by a
nostr pubkey rather than a domain. The relays, Blossom servers and the
/index.html fallback live in .nsite/config.json — review them before your
first deploy.
nsyte is a Deno CLI, not an npm package:
deno install -Afg jsr:@nsyte/cli # once
pnpm deploy:nsitensite gateways handle the SPA fallback for you.
Any static host works — Vercel, Netlify, Cloudflare Pages, S3, nginx — as long as you configure the SPA fallback.
- React Router v7 in framework mode with
ssr: false. Routes still getmeta(), layouts and typed params; data comes fromclientLoaderinstead of a serverloader. - applesauce v6 for everything
nostr:
EventStoreas the single source of truth, loaders that batch and deduplicate relay requests, and RxJS observables for reactive updates. - TipTap v3 for the editor, serializing to Markdown.
- Tailwind v4 and Radix UI for the interface.
app/
├── routes/ # One file per URL; clientLoader + component
├── services/ # Relay pool, event store, loaders, accounts, wallet, drafts
├── nostr/ # Actions (publish, comment, zap request) and queries
├── ui/ # Components; ui/nostr/* is the nostr-aware layer
├── hooks/ # useProfile, useEvent, useTimeline, useZaps, ...
└── featured.ts # Featured articles, highlights and authors, baked in
For the longer version, see CLAUDE.md and claudedocs/.
Dropping the server bought simplicity and censorship resistance, and cost two things:
- No server-rendered meta tags.
meta()runs in the browser, so crawlers and chat previews that do not execute JavaScript see the generic site title rather than the article's. There is no sitemap either — the URLs are nostr identifiers, not a list anyone could enumerate ahead of time. - First paint waits on relays. There is no cache in front of the network; a page shows its skeleton until the events arrive.
See LICENSE.