1 Overview
Most NIPs define shared formats. Every client reads them the same way. This NIP does the opposite. It gives an app a place to store its own private data on Nostr. No other app has to understand it.
The goal is a remoteStorage-like capability. Some apps do not want or do not need interoperability. Interoperability would not make sense for them. Nostr can still serve them as a generalized data store, in a "bring your own database" way.
The flow is simple. A user opens an app. The user inputs a preferred relay for storage. The app then stores its application-specific data on that relay. The app does not need to run its own backend. The user keeps control of the data.
The app does not even have to be a Nostr app. Any program with a Nostr keypair and a WebSocket can use a relay as its personal settings drawer.
2 The Two Kinds
This NIP defines two event kinds. Both carry free-form payloads. The content field and the other tags can be anything, in any format. Only the storage behavior differs.
| Kind | Storage class | Use |
|---|---|---|
| 30078 | addressable | One current value per d tag. The relay keeps only the latest event per pubkey + kind + d value. |
| 78 | regular | Many events of the same type. The relay stores all of them. |
Kind 30078 - one slot per key
Kind 30078 is an addressable event, as defined in NIP-01. Its d tag contains some reference to the app name and context. Any other arbitrary string is also allowed. The d value acts as the storage key. A new event with the same d value replaces the old one on the relay.
{
"kind": 30078,
"tags": [
["d", <app name + context, or any arbitrary string>],
<any other tags, in any format>
],
"content": <anything, in any format>,
...
}
Kind 78 - a growing collection
Kind 78 is a regular event, as defined in NIP-01. Use it when the app needs to store and query multiple events of the same type. Each new event is kept. Nothing replaces anything. Apps should use some unique tag to group these events together. Even a d tag works for this, since on a regular event it does not trigger replacement.
{
"kind": 78,
"tags": [
["d", <unique grouping tag chosen by the app>],
<any other tags, in any format>
],
"content": <anything, in any format>,
...
}
3 Use Cases
The source NIP lists three example uses. None of them requires other clients to understand the data:
- Personal settings. User preferences for Nostr clients, and for other apps unrelated to Nostr.
- Dynamic parameters. A way for client developers to propagate dynamic parameters to users. Users do not have to update the app.
- Personal private data. Data from apps that have nothing to do with Nostr. The user simply uses Nostr relays as a personal database.
Relays are public by default. Anyone who knows your pubkey can read your kind-30078 and kind-78 events. For private data, encrypt the content before you publish, for example with NIP-44, or use a relay that restricts reads.
The source NIP mentions "personal private data" as a use case but defines no encryption or access control. Privacy is left entirely to the app. Also note a term drift: the source calls kind 78 a "normal" event, while NIP-01 calls that storage class "regular". This manual uses "regular".
4 Worked Example
An imaginary podcast app called castcat stores the user's playback settings in one kind-30078 slot. It logs each finished episode as a kind-78 event. First, the settings event:
{
"id": "7c1f30aa9e…",
"pubkey": "22a12a12…",
"created_at": 1749762600,
"kind": 30078,
"tags": [["d", "castcat/settings"]],
"content": "{\"speed\":1.5,\"theme\":\"dark\",\"skip_intro\":true}",
"sig": "a91b2c3d…"
}
The session below shows the full round trip on the user's storage relay. The app publishes the slot, appends one log entry, and later reads everything back on a new device. Orange lines go to the relay. Blue lines come from the relay.
-- device 1: save settings and log one episode -- → ["EVENT", {"kind":30078, "tags":[["d","castcat/settings"]], "content":"{\"speed\":1.5,\"theme\":\"dark\",\"skip_intro\":true}", …}] ← ["OK", "7c1f30aa9e…", true, ""] → ["EVENT", {"kind":78, "tags":[["d","castcat/history"]], "content":"{\"episode\":\"s02e04\",\"finished_at\":1749762900}", …}] ← ["OK", "e44d1b90cc…", true, ""] -- device 2: restore state -- → ["REQ", "cfg", {"kinds":[30078], "authors":["22a12a12…"], "#d":["castcat/settings"]}] ← ["EVENT", "cfg", {"kind":30078, "content":"{\"speed\":1.5, …}", …}] ← ["EOSE", "cfg"] → ["REQ", "log", {"kinds":[78], "authors":["22a12a12…"], "#d":["castcat/history"]}] ← ["EVENT", "log", {"kind":78, "content":"{\"episode\":\"s02e04\", …}", …}] ← ["EOSE", "log"] → ["CLOSE", "cfg"] → ["CLOSE", "log"]
When the user changes the playback speed, the app publishes a new kind-30078 event with the same d value. The relay drops the old settings event. The kind-78 history keeps growing, one event per finished episode.
5 Version History
NIP-78 is a short spec and has barely changed since fiatjaf proposed it. Notable changes, newest first:
- 20262026-05Added kind
78, a regular event kind for apps that store and query multiple events of the same type. - 20242024-08Renamed "parameterized replaceable event" to "addressable event" in a repo-wide terminology sweep.
- 20232023Grammar and typo fixes; author lines removed from all NIPs.
- Origin2023-02fiatjaf published NIP-78: app-specific data with kind
30078.
Original Proposal - Credits
| Author | fiatjaf |
| First commit | 2023-02-23 — “NIP-78: app-specific data.” |
| Status | draft - optional |
| Source | 78.md on GitHub - full change history |
6 See Also
- NIP-01 — defines addressable and regular storage classes, the
dtag index, and the#dfilter used here. - NIP-44 — encrypted payloads; use it to keep "personal private data" actually private.
- NIP-65 — relay list metadata; a way to advertise which relays a user writes to.
- NIP-42 — client authentication; useful for a storage relay that restricts reads and writes.