Nosternity: Give Important Signed Events a Retention Policy

Decide which events should outlive a relay session

A signed event gives an application an identifiable statement from a key. A release announcement, report reference or operational note may need to be retrieved long after the session that first delivered it. Retaining every message indefinitely is a different requirement from retaining a selected class of important records.

Nosternity's current event-store code provides a concrete starting point for that selection. It checks signed events against kind and author policy, queues accepted records for an Aeternity contract, and distinguishes transaction submission from confirmation. Relay startup can read stored events back into its local cache.

This makes Nosternity worth evaluating as an event-retention component for Nostr application developers and operators. Its local relay interface still has unfinished behaviour, so the most focused pilot is the event-store path with non-sensitive fixtures.

What changed in the current source

The 5 October latest-apps.tar.gz snapshot supersedes the July snapshots used by an earlier version of this article. The supervisor now explicitly starts nosternity_event_store and nosternity_relay, alongside two shared damage_nostr identity workers. It expects the relay pool to be supervised by Damage rather than starting another pool itself.

The relay now verifies an event through damage_nostr_event:verify/1 before inserting it into ETS. It uses insert_new to suppress duplicate event IDs and asks the event store to retain a newly accepted event. These are present code paths. No current running-service or deployment result was supplied.

Select records before committing storage

The event store is disabled by default. When enabled, it supports kind and public-key policies, event-size limits, a bounded queue and batching. The source defaults select kinds 1, 7 and 30023; the default event-size limit is 65,536 bytes. Operators should choose the actual policy for their application.

Validation checks the required fields, event ID and signature through the shared verifier. Selection decides whether a valid event belongs in the retained set. Both checks matter: an authentic event may still be outside your storage policy.

The contract record includes the event content, tags and signature as well as its identifiers. It is not merely a digest commitment. Treat selected content as intended for the configured chain's visibility and retention; use public or explicitly approved records in the pilot.

Make confirmation visible to the caller

store_sync/1 reports a validation or queue decision. It does not wait for mining. With ae_event_store_confirm_writes=true, the batch is removed from the retry queue only when the tracked contract call reports confirmed. Submitted or uncertain outcomes remain retry cases. Disabling confirmation uses weaker submission-based dequeue semantics.

The following inspection calls are available in a configured running release:

nosternity_event_store:status().
nosternity_event_store:contract_id().
nosternity_event_store:get_event_count().

Use an event ID to correlate the application record, store query and chain receipt. A relay's ok response or a {queued, Id} response is insufficient evidence of persistence. Contract source, deployment configuration and live receipts were absent from the uploaded application-source archive.

Recovery has a precise current boundary

The relay can fetch stored events in pages and rehydrate its ETS cache. This is useful for rebuilding a local view of records that reached the contract. It does not recover every pending write: the event-store queue is held in process memory, so unconfirmed queued records have no local durable outbox in the reviewed module.

There is also a retry boundary at ingress. The relay inserts the event into ETS before its asynchronous store request, and duplicates are suppressed. A later queue rejection or unavailable contract therefore cannot be repaired by assuming an ordinary duplicate relay submission will enqueue the record again. An adoption pilot should explicitly exercise this case and define a retry/reconciliation path before promising lossless retention.

Use the right integration surface

The reviewed nosternity_relay still returns every event from its filter helper, and subscriber broadcasting remains a TODO. Its subscription handler logs a request without establishing the complete delivery path. The separate WebSocket handler also needs a full protocol interoperability test. This snapshot should not be advertised as a complete drop-in public Nostr relay.

The older nosternity_relay_client still tries to select tuples from erlang:processes(), whose elements are PIDs; that registry expression cannot supply the intended relay list. The application also has a separate note/thread HTTP viewer using the shared Nostr client/pool. These paths need to be tested individually rather than inheriting the bunker transport's earlier results.

A first retention pilot

Choose one allowed author and a small set of public fixture events. Verify acceptance, tamper rejection, author/kind exclusion and duplicate handling. Observe confirmed storage, restart and rehydrate the cache, then repeat while the contract is unavailable and while the queue is full. Record which events are retained, rejected, pending or lost across the restart.

The immediate adoption measure is whether a second process can retrieve the same confirmed event by ID after recovery. The next engineering milestone is a durable ingress/retry contract and completed relay subscription behaviour. That would strengthen the path from a useful event-store component to a broader relay product.