write_nostr is a browser-based long-form writing app for the Nostr protocol. It provides a distraction-free Markdown editor for composing, publishing, and managing NIP-23 long-form content (kind:30023) directly to Nostr relays, along with feeds for discovering articles from the network.
Built with Svelte 5 and SvelteKit, write_nostr runs as a fully static single-page app — no backend required. Authentication works via NIP-07 browser extensions, nsec keys, or passkeys (WebAuthn/PRF). Articles are rendered with live preview, support metadata (title, summary, featured image, tags), and auto-save to localStorage drafts. The app is installable as a PWA with offline support.
Whether you're a writer looking for a censorship-resistant publishing platform or a Nostr user who wants to read and engage with long-form content on the decentralized web, write_nostr offers a clean, focused experience.
- Write & Publish — Full-featured Markdown editor with live preview, formatting toolbar, metadata (title, summary, featured image, tags), and direct publishing to Nostr relays.
- Browse Articles — Three feed modes: All (global firehose), My Circle (articles from people you follow), and Top Articles (sorted by interaction score). Tag filtering, pagination, and manual refresh.
- Drafts — Auto-saving drafts to localStorage with a dedicated drafts manager.
- Authentication — Sign in via NIP-07 browser extension, paste an nsec key, or use a passkey (WebAuthn/PRF).
- Social — Follow/unfollow and block/unblock authors directly from the feed.
- Profiles — Kind:0 profile metadata fetched from relays, cached for performance.
- Translation — On-device article translation via Chrome's Built-in AI Translation API.
- PWA — Installable as a progressive web app with offline support.
| Layer | Technology |
|---|---|
| Framework | Svelte 5 + SvelteKit 2 (runes mode) |
| Build | Vite 8 + TypeScript 6 |
| Nostr | nostr-tools — SimplePool, event signing, NIP-19 encoding/decoding |
| Passkeys | nostr-passkey — WebAuthn/PRF-based key management |
| Markdown | marked — rendering |
| Caching | idb — IndexedDB cache layer |
| Adapter | @sveltejs/adapter-static — static SPA output |
- NIP-01 — Basic protocol & relay communication
- NIP-02 — Contact lists (kind:3)
- NIP-07 — Browser extension signer
- NIP-19 — bech32-encoded entities (
nsec,npub,naddr,nprofile,nevent) - NIP-23 — Long-form content (kind:30023)
- NIP-51 — Lists (kind:10000 mute/block lists)
- NIP-65 — Relay list metadata (kind:10002)
npm install
npm run dev # Development server with hot reloadOpen the URL printed by Vite (usually http://localhost:5173).
npm run build # Production build to ./build
npm run preview # Preview the production build locallynpm run deploy -- --host user@host --domain example.comThe deploy script:
- Builds the project
- Syncs to a remote server via rsync over SSH
- Installs a systemd service running the built files via the included Python SPA HTTP server
- Optionally configures Caddy or nginx as a reverse proxy with TLS
- Local:
npm,rsync - Remote:
python3,systemctl,ss,curl,rsync,sudo
Environment variables: WRITE_NOSTR_SSH_TARGET, WRITE_NOSTR_DOMAIN, WRITE_NOSTR_PORT, etc.
npm run checksrc/
├── app.html # HTML shell
├── app.css # Global styles (light/dark mode)
├── lib/
│ ├── components/ # Svelte components (Editor, ArticleCard, etc.)
│ ├── stores/ # Svelte stores (auth, relays, drafts, social, profiles)
│ ├── nostr/ # Nostr logic (fetch, publish, resolve, profiles)
│ └── utils/ # Utilities (markdown, NIP-19, translation)
├── routes/
│ ├── +page.svelte # Main feed page
│ ├── +layout.svelte # Root layout
│ ├── new/ # Editor (/new)
│ ├── drafts/ # Drafts manager (/drafts)
│ ├── settings/ # Settings (/settings)
│ └── article/[naddr]/ # Article view (/article/[naddr])
static/
├── brand/ # Logo assets
├── pwa/ # PWA icons
├── manifest.webmanifest # PWA manifest
└── sw.js # Service worker
wss://relay.damus.iowss://relay.nostr.bandwss://nos.lolwss://relay.primal.net
Relays can be managed in Settings after login.