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.
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
- NostrEventStore contract
- Relay validation, deduplication and recovery hooks
- Supervisor and required child processes
- Shared Aeternity contract lifecycle helper
- event-store integration guide
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.