v0.8.0-rc1
Pre-releaseGRAIN🌾 v0.8.0
A Nostr client — and the library it's built on
Release candidate · 6.18.2026
Full Changelog: v0.7.1...v0.8.0-rc1
v0.7 turned the relay into something you operate from a browser. v0.8 turns grain into a full Nostr client — an importable Go client library that speaks the outbox model, and a web client built on top of it that manages your profile, relays, media servers, and encryption, all signed with your own key.
The headline is client/core: a standalone, outbox-model client engine you can import into your own Go app. It owns a shared relay pool, resolves each user's relay lists, and routes every read and publish to the right relays under the gossip / outbox model — you read a user's notes from their outbox, and a reply you publish reaches the parent author's inbox. grain's own web frontend is the reference consumer: everything the UI does, it does through this library, so the client is both a usable app and a worked example of building on the engine.
Native NIP-44 encryption (v2 + v3), NIP-42 relay AUTH, NIP-65/17/51/37 relay lists, NIP-89 client tags, and Blossom + NIP-96 media all landed this cycle.
📦 The client library (client/core)
An importable, outbox-model Nostr client engine in pure Go — no cgo, no web/HTTP dependencies. A downstream app builds its own client on it instead of reimplementing relay routing.
- Outbox routing, automatic. Name the intent and the engine picks the relays: reads come from a user's NIP-65 outbox, a published note reaches the author's outbox plus each mentioned recipient's inbox, and metadata resolves from the indexers. A leased connection pool keeps the routing additive — switching a session's relays adds connections rather than tearing the pool down.
- A role model for relays. Every relay carries a
Rolebitmask — outbox / inbox / DM-inbox (per-target, from NIP-65 / NIP-17), search / blocked / favorite / private (the user's own NIP-51/37 lists), and locally-configured indexer / broadcast / trusted roles. - Streaming fetches. A multi-relay streaming primitive delivers events on a channel as each relay answers, de-duplicated by id — the basis for lazy-hydrating feeds.
- Pluggable seams. Supply your own
Signer(local key, or bring a NIP-46 / hardware signer),Logger(any*slog.Logger), andRelayListStore(default in-memory; plug a database for persistence). A read-only context needs no signer at all. context.Contextthroughout. Both the read/fetch and publish paths take a context for caller-set deadlines and cancellation.- Fixed-relay opt-out. For a pinned single-/few-relay client, pin a fixed read/write set — which deliberately disables the outbox model. Off by default and discouraged.
The frontend's header shows a live x/y relays pool indicator, and /api/v1/client/status exposes the pool's stats.
For developers — the importable surface
import "github.com/0ceanslim/grain/client/core"
client := core.NewClient(core.DefaultConfig()) // owns the shared pool
signer, _ := core.NewEventSigner(privKeyHex) // or your own Signer
uc := client.NewUserContext(signer.PublicKey(), core.WithSigner(signer))
notes := uc.FetchNotes(ctx, author, core.WithLimit(20)) // → author's outbox
reply, results, _ := uc.Reply(ctx, parent, "well said!") // → outbox ∪ parent's inboxUserContextfacade with intent methods (FetchNotes/StreamNotes/Reply/Publish/SignAndPublish).- Inspectable routing — ask the engine which relays it would use without performing the operation.
- The full surface is documented in docs/client-library-guide.md, with compile-checked examples in
client/core/example_test.gothat run in CI, so the public API can't drift without breaking the build. - Architecture + rationale: the outbox relay-pool design doc. Tracked under #56 and #77.
👤 Your profile, edited in place
The profile page is now a real editor for your own kind-0 metadata.
- Click into your name, bio, picture, banner, or any field and edit in place — no separate form. An Advanced editor exposes the raw content fields and drag-reorderable tags.
- Edits are signed and published with your key, routed to the indexers (so anyone can find your metadata) plus your own relays.
- A live publish toast counts up per-relay acceptances (NIP-20
OK) as each relay answers — and surfaces signer errors instead of failing silently.
📡 Relay management
A new relay-management surface in settings, plus a browser for every relay grain knows about.
- Per-category relay lists — outbox/inbox (NIP-65), DM (NIP-17), and the NIP-51/37 lists — each editable and published with one signed event. A fixed-relay override is there for pinned clients.
- Known-relays browser — browse every relay the engine has seen, with live status, NIP-11 info on expand (name, software, supported NIPs, auth/payment flags), and a "sort: fastest" TCP-latency ping. Stage any relay straight into one of your lists.
- Add-relay autocomplete — every add-relay input completes from the known set as you type.
- Login hydration warms the relay-list and media caches at sign-in, so settings render instantly instead of resolving cold.
🖼️ Media servers & uploads
Manage your media servers and upload straight from the client.
- Resolve and edit your Blossom (kind 10063) and legacy NIP-96 (kind 10096) server lists from settings.
- A reusable upload module with a pre-upload modal: pick your primary server, mirror to others with per-server checkboxes, preview the file, and see an ephemeral-storage warning before you commit. Wired into both the profile editor and the admin image fields.
- Uploads are signed client-side — sha256 + a Blossom (BUD-01) or NIP-96 authorization, signed with your key — and stream a live per-server progress toast.
🔐 Encryption — NIP-44 (v2 + v3)
Native NIP-44 conversation encryption, validated against the official test vectors.
- v2 is the deployed standard and the default; v3 (the in-progress draft, opt-in) binds the event kind and a scope into the MAC.
- This is the primitive behind private relay lists: a NIP-37 (10013) / encrypted NIP-51 list now has a decrypt button that reveals its private entries on demand, with your signer — grain itself never sees the plaintext.
🛡️ Relay AUTH — NIP-42
The client now answers relay AUTH challenges with your signer.
- Relays that issue a NIP-42 challenge show up in a "relays requesting AUTH" list; one click signs a kind-22242 event and authenticates you for the session. Authenticated relays stay trusted until you remove them, and a fresh challenge re-prompts automatically.
🏷️ Client tags — NIP-89
- Published events are stamped with a
["client", "grain"]tag, and any foreign client tag is stripped first so a re-published event never leaks another app's attribution. There's a user-facing opt-out slider, plus admin defaults.
🏠 Live homepage feed
- The homepage streams events hitting this relay live over a direct WebSocket — author avatar + name, readable kind label, relative time, and a snippet for notes. Newest sort to the top; scroll to lazy-load older events. Click through to the event or the author's profile.
- Built on a new SSE push channel (relay → browser) and a live-sync own-event subscription, so your own actions reflect on the page as they happen.
🆕 New-user onboarding
- Sign in with a brand-new key and the profile page opens as an empty, editable profile (instead of an error), a permanent "set up relays" banner guides you to configure your relays, and you can publish your first metadata immediately.
📚 Docs & dashboard
- The API docs at
/api/docsnow cover the client endpoints, and "Try it out" signs its NIP-98 auth with your connected signer — no pasting tokens. - The admin dashboard gained a client-config section (
grain_updateclient), and Relay information split into its own section with Operations moved to the top.
For operators — upgrade notes
- No config migration is required; all client-library seams default to today's behavior (in-memory caches, grain's own logging).
- The OpenAPI spec is generated from swag annotations at build time and embedded via
//go:embed; the build seeds a placeholder so generation is self-contained. - This is a release candidate — please file anything you hit before the final
v0.8.0.
Issues: #56 · #74 · #77 · #80 · #83 · #87 · #90 · #98 · #99 · #100 · #101