Nosternity: Give Important Signed Events a Retention Policy

An append-only contract, event-store adapter and relay recovery path for selected signed Nostr records, with explicit pending-queue and validation limits.

Updated Read as Markdown ↗

Give an important event a longer life

A signed release announcement or incident note may need to remain available long after the relay session that delivered it. The application needs a retention policy: which records to keep, who may add them, where they live, and how another process retrieves them later.

Nosternity connects Nostr event handling with an Aeternity storage design. Nostr supplies the signed record; the contract supplies an append-only archive with lookup by event ID and insertion order. This can support an auditable history of selected public announcements, article versions or report references.

The source reviewed on 7 October includes the event-store adapter, its supervisor and relay connections, and focused validation tests. The adapter selects events, queues batches, submits contract writes and provides read operations. This resolves the source-availability question behind the earlier missing-module report.

Publication and acceptance remain separate steps. The adapter and its tests are absent from the verified public revision linked below; the dated integration review records the reviewed file identities. Its pending queue is also held in memory, so the implementation is not yet a durable outbox for every event acknowledged by the relay.

Separate delivery from retention

A relay connection answers a delivery question: can an event reach another participant? Retention answers a different question: can that participant retrieve the event later, after the original process or relay session is gone?

Consider a release announcement that refers to a DamageBDD report. Keeping the event preserves the publisher's statement and its references. The report and any supporting attachment still need their own storage policy. An event archive does not automatically preserve every file mentioned by its events.

What the contract implements

The repository's NostrEventStore contract stores the full event record: ID, author, creation time, kind, tags, content and signature. It indexes records globally and by kind. An existing event ID is skipped on insertion, which allows a caller to retry a batch without creating a second copy of the same record.

Only the deploying account can write. Read entry points are public. The contract does not repeat Nostr signature verification on chain; that check belongs to the application before an authorised write. The owner-controlled write path is therefore part of the trust model.

An append-only archive keeps historical IDs. It does not, by itself, implement every Nostr replacement or deletion rule. An application presenting the current version of a record must apply its own query policy over that history. Choose public or explicitly approved records because full event content, rather than only a digest, is committed to the contract.

What the relay implements

The relay verifies an incoming event through damage_nostr_event:verify/1 and inserts a new ID into ETS, Erlang's in-memory table store. It then calls the event-store adapter asynchronously. At startup, its recovery path asks that adapter for pages of stored events and uses them to rebuild the local table.

The order matters. The relay records an ID before requesting persistence, and later submissions of the same ID are suppressed. An ok from the relay therefore establishes local acceptance, not confirmed storage. A persistence failure needs an explicit retry or reconciliation path; submitting the same event again through the relay is not sufficient by itself.

Subscription handling is also incomplete: the filter helper accepts every event, and subscriber broadcasting remains a TODO. The separate WebSocket and HTTP interfaces need their own interoperability checks. This revision does not provide a complete replacement for a public Nostr relay.

What the adapter now supplies

The reviewed apps/nosternity/src/nosternity_event_store.erl is a supervised gen_server. Before accepting selected events, it checks required fields, lowercase hexadecimal identifiers, the canonical event ID, the signature and the encoded event-size limit. Author and kind policies decide which records are candidates for retention.

Accepted events enter a bounded pending queue with duplicate IDs suppressed. The worker flushes batches through the shared damage_ae_contract helper. With ae_event_store_confirm_writes enabled, it removes a batch only when the tracked call returns confirmed. Submitted, unknown and failed outcomes remain retryable. The contract's event-ID deduplication prevents repeated insertion of the same event, but repeated transactions can still incur fees.

This confirmation refers to the helper observing a mined successful call; it is not a separately demonstrated finality or reorganisation guarantee. With confirmation disabled, the adapter accepts the weaker submission result and can remove work before mining is observed.

store_sync/1 reports validation or queue admission, not chain confirmation. status/0 exposes queue depth, policy, contract identity, confirmation mode and the last error. Read operations support lookup by ID, counts, kind indexes and pages of up to 100 events, which the relay's startup path uses to rebuild its local table.

Configure the policy and the recovery boundary

The event-store integration guide describes the configuration keys. Their defaults are implemented in the reviewed adapter:

Setting Default Meaning
ae_event_store_enabled false Retention is opt-in
ae_event_store_auto_deploy false Use a configured contract unless deployment is explicitly enabled
ae_event_store_confirm_writes true Retain a batch until a confirmed result is observed
ae_event_store_kinds 1, 7, 30023 Select notes, reactions and long-form articles
ae_event_store_pubkeys all Configure an author list to restrict paid storage
ae_event_store_queue_max 5,000 Cap accepted pending events
ae_event_store_batch_size 10 Bound the number of events in each write batch

The configuration example named by the guide is still absent from the verified public revision. Use the documented keys with a matching implementation and persist the contract address in application configuration. Automatic deployment remembers an address only for the current VM.

The important remaining storage boundary is the pending queue. It is an Erlang process-state queue with no disk journal or checkpoint. If the adapter restarts, unconfirmed work can be lost while the relay keeps its already-seen IDs. The supervisor uses one_for_one, so an adapter restart does not itself clear the relay's deduplication table. Re-publishing an already-seen event through that relay will not rebuild the lost pending work.

A durable outbox or explicit reconciliation mechanism is therefore needed for a promise that every accepted event will eventually be retained. Startup hydration recovers events already on chain; it cannot recover an event that was accepted locally but never stored. Queue-full and unavailable-contract outcomes also need to be visible to the service deciding what was retained.

Evaluate retention with a small event set

After including the adapter in a matching checkout, choose one allowed author and a small set of public fixture events. Verify signature rejection, author/kind exclusions, duplicate handling and a confirmed contract write. Read the same event back by ID from a second process.

Then restart the adapter alone and the complete service before and after confirmation. Test a full queue, an unavailable chain and an uncertain submission result. The outcome should identify which events are retained, pending, refused or require retry. This is how the project can demonstrate recovery rather than merely successful submission.

The reviewed nosternity_event_store_tests.erl exercises valid signatures, tampered events, author/kind policy and event-size rejection. It does not exercise batch submission, confirmation retries, pending-queue recovery or relay interoperability. Those tests were inspected, not executed here.

The useful adoption result is a record that another process can retrieve after recovery, with its original event identity intact. Retention cost, storage growth and query behavior should be measured for the event classes your application actually intends to keep.

Explore the implementation

These links identify the published contract and integration interfaces. The adapter and test file fingerprints are recorded in the 7 October source review because they are not present at that public revision. No compilation, contract deployment, live persistence or recovery test was performed for this documentation update.

Search documentation

Search titles, summaries and document paths.