Chapter 78 - App Storage
78
Nostr Implementation Possibility - Hex № 78

Arbitrary Custom App Data

Turn any relay into a bring-your-own database for apps that do not need interoperability.

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.

Hint!

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.

App device 1 User's relay chosen by user Same app device 2 kind 30078 REQ + #d
Fig. 78-1 — Bring your own database. The app writes app-specific events to the user's relay and reads them back anywhere.

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.

Kinds defined by this NIP
KindStorage classUse
30078addressableOne current value per d tag. The relay keeps only the latest event per pubkey + kind + d value.
78regularMany 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.

Wire format - kind 30078
{
  "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.

Wire format - kind 78
{
  "kind": 78,
  "tags": [
    ["d", <unique grouping tag chosen by the app>],
    <any other tags, in any format>
  ],
  "content": <anything, in any format>,
  ...
}
kind 30078 — overwrite d = "myapp/theme" v1 (dropped) v2 (kept) kind 78 — append d = "myapp/log" entry 1 entry 2 entry 3 The relay keeps every kind-78 event. It keeps only the latest 30078 per d value.
Fig. 78-2 — Two storage behaviors. Kind 30078 is a named slot. Kind 78 is an append-only collection.

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.
Warning!

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.

Tech Note

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:

Event - settings slot (kind 30078)
{
  "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.

Session transcript - wss://storage.example.com
-- 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

Authorfiatjaf
First commit2023-02-23 — “NIP-78: app-specific data.”
Statusdraft - optional
Source78.md on GitHub - full change history

6 See Also

  • NIP-01 — defines addressable and regular storage classes, the d tag index, and the #d filter 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.