Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nostr Php Laravel Package

swentel/nostr-php

A PHP library for building Nostr clients and tools, with support for keys, events, relays, filters, and NIP implementations. It offers a clean, object-oriented API for publishing notes, subscribing to relays, and handling cryptographic signing in Laravel or plain PHP projects.

Deep Wiki
Context7

Getting Started

Install and inspect

composer require swentel/nostr-php
composer check-platform-reqs   # confirms ext-gmp / crypto backend requirements

Before writing any code, open two things in this order:

  1. The examples/ directory in the repo — it contains runnable scripts for keys, events, relays, metadata, DMs and zaps. They are the fastest way to learn the actual method names in the version you installed.
  2. src/ namespace layout — it maps almost 1:1 to Nostr concepts, so you can navigate by NIP: Key\Key, Event\Event, Filter\Filter, Relay\Relay, Relay\RelayResponse, Subscription\Subscription, Message\*, Nip19\Bech32.

First use case: sign a note and publish it. This exercises the whole happy path (key → event → relay) without needing a subscription loop.

use swentel\nostr\Key\Key;
use swentel\nostr\Event\Event;
use swentel\nostr\Relay\Relay;

$key = new Key();
$privateKey = $key->generatePrivateKey();          // 64-char hex
$publicKey  = $key->getPublicKey($privateKey);

$event = new Event();
$event->setKind(1);
$event->setContent('Hello from PHP');
$event->addTag(['t', 'php']);
$event->sign($privateKey);                         // sets id, pubkey, sig

$relay = new Relay('wss://relay.damus.io');
$relay->setEvent($event);
$response = $relay->send();                        // OK / NOTICE frame

if (! $response->isSuccess()) {
    logger()->warning('Relay rejected event', ['msg' => $response->getMessage()]);
}

If that runs, everything else is variation: different kinds, different filters, more relays.

Implementation Patterns

Treat the library as three layers and don't mix them. Cryptography (Key, signing), event construction (Event, tags, kinds), and transport (Relay, Filter, messages) are independent. Most bugs come from mixing them — e.g. reaching for Relay when you only needed to sign offline.

Key management. Always store the hex private key in your DB (encrypted cast), and convert to nsec only at the UI boundary. Keep a public_key column and an index on it — it's the join key for everything.

$nsec = $key->convertToNsec($privateKey);
$npub = $key->convertToNpub($publicKey);   // bech32 for display
$hex  = $key->convertToHex($nsec);         // back to hex before signing

Custom event kinds via a small factory. Rather than scattering setKind() calls, wrap them:

class NostrEventFactory
{
    public function note(string $content): Event
    {
        return (new Event())->setKind(1)->setContent($content);
    }

    public function metadata(array $profile): Event
    {
        $event = (new Event())->setKind(0)->setContent(json_encode($profile));
        return $event;
    }
}

Set every tag, then call sign() last — id is derived from the serialized event, so any mutation after signing invalidates the signature.

Reading: filter → fan out → collect. The transport is blocking and per-relay, so the natural pattern is one Relay per URL, closed as soon as you hit EOSE (end of stored events) or your limit.

$filter = (new Filter())
    ->setKinds([1])
    ->setAuthors([$publicKey])
    ->setLimit(20)
    ->setSince(time() - 86400);

foreach ($relays as $url) {
    try {
        $relay = new Relay($url);
        $relay->setFilter($filter);
        $response = $relay->connect();
        foreach ($response->getContent() as $message) {
            // $message is a decoded relay frame; hydrate into Event
        }
        $relay->close();
    } catch (\Throwable $e) {
        report($e);   // one dead relay must not kill the request
    }
}

Laravel integration.

  • Bind a NostrService in a service provider holding your key + relay list from config/nostr.php.
  • Push publishing into a queued job (PublishNostrEvent), one job per relay, with $tries = 3 and backoff(). Relay sockets are slow and flaky; never publish inline in an HTTP request.
  • Cache relay responses by filter hash for a short TTL, since feeds are read-heavy and relays rate-limit.
  • Use the Http facade for NIP-05 (/.well-known/nostr.json) verification rather than reimplementing it.

Composition over heredocs. Use the Message\* classes (EventMessage, ReqMessage, CloseMessage) and a Subscription for request IDs instead of hand-building JSON strings — it guarantees the ["REQ", id, filter] shape relays expect.

Gotchas and Tips

Hex vs bech32 is the #1 failure mode. sign() wants the 64-char hex key; nsec1... will produce an invalid or exception-throwing signature. Round-trip through convertToHex() at every boundary where user input is involved.

Relay responses are not one object. A single send()/connect() can yield EVENT, EOSE, OK, NOTICE, CLOSED and AUTH frames. Always switch on the response type rather than assuming success — isSuccess() alone won't tell you a relay silently dropped you via CLOSED.

Verify what you receive. Events pulled from relays are untrusted input. Call verify() on each Event before persisting; without it, a malicious relay can hand you forged signatures and you'll store them as gospel.

Clock skew kills events. Relays reject events with created_at far in the future (usually > ~15 min). If your app server drifts or you backdate imports, clamp created_at to time() before signing.

Filter quirks are relay-specific. limit is a per-relay cap, many relays cap ids/authors at a few hundred entries, and since/until are Unix integers, not datetime strings. Sending an over-large filter often returns an empty result with no error at all — debug by testing the same JSON with nak or a web client first.

Blocking sockets in a request lifecycle. Wrap every relay call with an explicit timeout and a try/catch; a single unresponsive relay can hang a PHP-FPM worker for the full connect timeout. Combine with pcntl-free parallelisation only via queues, not via async libraries the package doesn't support.

NIP-04 DMs are legacy. The Message encryption helpers implement NIP-04, which leaks metadata and is superseded by NIP-17/NIP-44 gift wrapping. Use it only for compatibility; don't build new features on it.

Extension points worth knowing:

  • Subclass Event when a kind needs typed accessors (getTagsByName('e'), parsed content), but keep toJson() behaviour intact.
  • RelayResponse gives you raw frames — that's your escape hatch for handling AUTH (NIP-42) challenges manually if the version you're on doesn't automate them.
  • Nip19\Bech32 is reusable for any bech32 work (note1, nevent1, naddr1) beyond keys — handy for building shareable URLs.

Pin the version in composer.json. The library is pre-1.0 in spirit (API moves between releases, method names around Relay::send()/connect() have shifted) and it carries a crypto dependency. Lock it, and re-read examples/ after each upgrade rather than trusting your memory of the API.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table