clone

every nostr nip as a chapter of an illustrated, early-2000s printed game manual

main
nip.guide / site / nips / 78.html
78.html 17.4K
1<!DOCTYPE html>2<html lang="en">3<head>4<meta charset="utf-8">5<meta name="viewport" content="width=device-width, initial-scale=1">6<title>NIP-78 - Arbitrary Custom App Data — Nostr Instruction Manual</title>7<meta name="description" content="NIP-78: arbitrary custom app data. Kind 30078 and kind 78 turn any Nostr relay into a bring-your-own database for apps that do not need interoperability.">8<link rel="preload" href="../assets/fonts/michroma-400-latin.woff2" as="font" type="font/woff2" crossorigin>9<link rel="preload" href="../assets/fonts/exo2-var-latin.woff2" as="font" type="font/woff2" crossorigin>10<link rel="preload" href="../assets/fonts/sairacondensed-600-latin.woff2" as="font" type="font/woff2" crossorigin>11<link rel="preload" href="../assets/fonts/sairacondensed-700-latin.woff2" as="font" type="font/woff2" crossorigin>12<link rel="preload" href="../assets/fonts/sairacondensed-500-latin.woff2" as="font" type="font/woff2" crossorigin>13<link rel="preload" href="../assets/fonts/sharetechmono-400-latin.woff2" as="font" type="font/woff2" crossorigin>14<link rel="preload" href="../assets/fonts/exo2-400i-latin.woff2" as="font" type="font/woff2" crossorigin>15<link rel="icon" href="../assets/favicon.svg" type="image/svg+xml">16<link rel="stylesheet" href="../assets/manual.css">17</head>18<body>1920<svg width="0" height="0" style="position:absolute">21 <defs>22 <marker id="arr" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">23 <path d="M1,1 L8,4.5 L1,8 Z" fill="#f26a00"/>24 </marker>25 <marker id="arrb" markerWidth="9" markerHeight="9" refX="7" refY="4.5" orient="auto">26 <path d="M1,1 L8,4.5 L1,8 Z" fill="#2e5f9e"/>27 </marker>28 </defs>29</svg>3031<div class="page">32 <span class="staple s1"></span><span class="staple s2"></span>3334 <header class="band">35 <a class="logo" href="../index.html">NOSTR<b>▸</b>PROTOCOL MANUAL</a>36 <span class="booklet-note">Chapter 78 - App Storage</span>37 </header>38 <nav class="tabs">39 <a href="../index.html">Contents</a>40 <a href="#overview" class="on">Overview</a>41 <a href="#spec">Spec</a>42 <a href="#examples">Examples</a>43 <a href="#history">History</a>44 </nav>4546 <div class="chapter">47 <div class="nipno" data-n="78">78</div>48 <div class="titles">49 <div class="kicker">Nostr Implementation Possibility - Hex № 78</div>50 <h1>Arbitrary Custom App Data</h1>51 <p class="sub">Turn any relay into a bring-your-own database for apps that do not need interoperability.</p>52 </div>53 </div>5455 <main class="spread">56 <div class="main">5758 <h2 class="bar" id="overview"><span class="num">1</span> Overview</h2>59 <p>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.</p>60 <p>The goal is a <a href="https://remotestorage.io/">remoteStorage</a>-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.</p>61 <p>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.</p>6263 <div class="box hint">64 <span class="tag">Hint!</span>65 <p>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.</p>66 </div>6768 <figure class="diagram">69 <div class="frame">70 <svg viewBox="0 0 560 170" role="img" aria-label="An app stores app-specific events on the user's chosen relay and reads them back on another device">71 <rect class="d-actor" x="10" y="30" width="130" height="50" rx="8"/>72 <text class="d-actor-label" x="75" y="52" text-anchor="middle">App</text>73 <text class="d-note" x="75" y="69" text-anchor="middle">device 1</text>74 <rect class="d-actor" x="220" y="60" width="120" height="50" rx="8"/>75 <text class="d-actor-label" x="280" y="82" text-anchor="middle">User's relay</text>76 <text class="d-note" x="280" y="99" text-anchor="middle">chosen by user</text>77 <rect class="d-actor" x="420" y="90" width="130" height="50" rx="8"/>78 <text class="d-actor-label" x="485" y="112" text-anchor="middle">Same app</text>79 <text class="d-note" x="485" y="129" text-anchor="middle">device 2</text>80 <path class="d-msg d-flow" d="M142,55 C185,58 190,72 217,80"/>81 <path class="d-msg blue d-flow" d="M342,90 C385,98 390,105 417,112"/>82 <text class="d-label" x="168" y="48" text-anchor="middle">kind 30078</text>83 <text class="d-label" x="382" y="82" text-anchor="middle">REQ + #d</text>84 <circle r="4" fill="#f26a00">85 <animateMotion dur="2.6s" repeatCount="indefinite" path="M142,55 C185,58 190,72 217,80"/>86 </circle>87 <circle r="4" fill="#2e5f9e">88 <animateMotion dur="2.6s" begin="1.3s" repeatCount="indefinite" path="M342,90 C385,98 390,105 417,112"/>89 </circle>90 </svg>91 </div>92 <figcaption>Fig. 78-1 — <b>Bring your own database.</b> The app writes app-specific events to the user's relay and reads them back anywhere.</figcaption>93 </figure>9495 <h2 class="bar" id="spec"><span class="num">2</span> The Two Kinds</h2>96 <p>This NIP defines two event kinds. Both carry free-form payloads. The <code>content</code> field and the other <code>tags</code> can be anything, in any format. Only the storage behavior differs.</p>9798 <div class="tscroll"><table class="ref">99 <caption>Kinds defined by this NIP</caption>100 <tr><th>Kind</th><th>Storage class</th><th>Use</th></tr>101 <tr><td class="btn">30078</td><td>addressable</td><td>One current value per <code>d</code> tag. The relay keeps only the latest event per <code>pubkey</code> + <code>kind</code> + <code>d</code> value.</td></tr>102 <tr><td class="btn">78</td><td>regular</td><td>Many events of the same type. The relay stores all of them.</td></tr>103 </table></div>104105 <h3>Kind 30078 - one slot per key</h3>106 <p>Kind <code>30078</code> is an <em>addressable</em> event, as defined in <a href="01.html">NIP-01</a>. Its <code>d</code> tag contains some reference to the app name and context. Any other arbitrary string is also allowed. The <code>d</code> value acts as the storage key. A new event with the same <code>d</code> value replaces the old one on the relay.</p>107 <div class="crt">108 <div class="titlebar">Wire format - kind 30078</div>109<pre>{110 <span class="k">"kind"</span>: 30078,111 <span class="k">"tags"</span>: [112 [<span class="s">"d"</span>, <span class="c">&lt;app name + context, or any arbitrary string&gt;</span>],113 <span class="c">&lt;any other tags, in any format&gt;</span>114 ],115 <span class="k">"content"</span>: <span class="c">&lt;anything, in any format&gt;</span>,116 ...117}</pre>118 </div>119120 <h3>Kind 78 - a growing collection</h3>121 <p>Kind <code>78</code> is a regular event, as defined in <a href="01.html">NIP-01</a>. 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 <code>d</code> tag works for this, since on a regular event it does not trigger replacement.</p>122 <div class="crt">123 <div class="titlebar">Wire format - kind 78</div>124<pre>{125 <span class="k">"kind"</span>: 78,126 <span class="k">"tags"</span>: [127 [<span class="s">"d"</span>, <span class="c">&lt;unique grouping tag chosen by the app&gt;</span>],128 <span class="c">&lt;any other tags, in any format&gt;</span>129 ],130 <span class="k">"content"</span>: <span class="c">&lt;anything, in any format&gt;</span>,131 ...132}</pre>133 </div>134135 <figure class="diagram">136 <div class="frame">137 <svg viewBox="0 0 560 180" role="img" aria-label="Kind 30078 writes overwrite the same slot; kind 78 writes append to a growing list">138 <text class="d-actor-label" x="10" y="20">kind 30078 — overwrite</text>139 <rect class="d-actor" x="10" y="32" width="150" height="34" rx="6"/>140 <text class="d-label" x="85" y="53" text-anchor="middle">d = "myapp/theme"</text>141 <rect x="230" y="32" width="120" height="34" rx="6" fill="#7d8794" opacity="0.35"/>142 <text class="d-label" x="290" y="53" text-anchor="middle">v1 (dropped)</text>143 <rect x="400" y="32" width="120" height="34" rx="6" fill="#f26a00"/>144 <text class="d-label" x="460" y="53" text-anchor="middle" fill="#fff">v2 (kept)</text>145 <path class="d-msg" d="M162,49 L227,49"/>146 <path class="d-msg" d="M352,49 L397,49"/>147 <text class="d-actor-label" x="10" y="106">kind 78 — append</text>148 <rect class="d-actor" x="10" y="118" width="150" height="34" rx="6"/>149 <text class="d-label" x="85" y="139" text-anchor="middle">d = "myapp/log"</text>150 <rect x="230" y="118" width="88" height="34" rx="6" fill="#2e5f9e"/>151 <text class="d-label" x="274" y="139" text-anchor="middle" fill="#fff">entry 1</text>152 <rect x="330" y="118" width="88" height="34" rx="6" fill="#2e5f9e"/>153 <text class="d-label" x="374" y="139" text-anchor="middle" fill="#fff">entry 2</text>154 <rect x="430" y="118" width="88" height="34" rx="6" fill="#2e5f9e"/>155 <text class="d-label" x="474" y="139" text-anchor="middle" fill="#fff">entry 3</text>156 <path class="d-msg blue" d="M162,135 L227,135"/>157 <text class="d-note" x="10" y="172">The relay keeps every kind-78 event. It keeps only the latest 30078 per d value.</text>158 </svg>159 </div>160 <figcaption>Fig. 78-2 — <b>Two storage behaviors.</b> Kind 30078 is a named slot. Kind 78 is an append-only collection.</figcaption>161 </figure>162163 <h2 class="bar" id="usecases"><span class="num">3</span> Use Cases</h2>164 <p>The source NIP lists three example uses. None of them requires other clients to understand the data:</p>165 <ul>166 <li><strong>Personal settings.</strong> User preferences for Nostr clients, and for other apps unrelated to Nostr.</li>167 <li><strong>Dynamic parameters.</strong> A way for client developers to propagate dynamic parameters to users. Users do not have to update the app.</li>168 <li><strong>Personal private data.</strong> Data from apps that have nothing to do with Nostr. The user simply uses Nostr relays as a personal database.</li>169 </ul>170171 <div class="box warn">172 <span class="tag">Warning!</span>173 <p>Relays are public by default. Anyone who knows your pubkey can read your kind-30078 and kind-78 events. For private data, encrypt the <code>content</code> before you publish, for example with <a href="44.html">NIP-44</a>, or use a relay that restricts reads.</p>174 </div>175176 <div class="box tech">177 <span class="tag">Tech Note</span>178 <p>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 <a href="01.html">NIP-01</a> calls that storage class "regular". This manual uses "regular".</p>179 </div>180181 <h2 class="bar" id="examples"><span class="num">4</span> Worked Example</h2>182 <p>An imaginary podcast app called <code>castcat</code> 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:</p>183184 <div class="crt">185 <div class="titlebar">Event - settings slot (kind 30078)</div>186<pre>{187 <span class="k">"id"</span>: <span class="s">"7c1f30aa9e…"</span>,188 <span class="k">"pubkey"</span>: <span class="s">"22a12a12…"</span>,189 <span class="k">"created_at"</span>: 1749762600,190 <span class="k">"kind"</span>: 30078,191 <span class="k">"tags"</span>: [[<span class="s">"d"</span>, <span class="s">"castcat/settings"</span>]],192 <span class="k">"content"</span>: <span class="s">"{\"speed\":1.5,\"theme\":\"dark\",\"skip_intro\":true}"</span>,193 <span class="k">"sig"</span>: <span class="s">"a91b2c3d…"</span>194}</pre>195 </div>196197 <p>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.</p>198199 <div class="crt">200 <div class="titlebar">Session transcript - wss://storage.example.com</div>201<pre><span class="c">-- device 1: save settings and log one episode --</span>202<span class="send">→ ["EVENT", {"kind":30078, "tags":[["d","castcat/settings"]],203 "content":"{\"speed\":1.5,\"theme\":\"dark\",\"skip_intro\":true}", …}]</span>204<span class="recv">← ["OK", "7c1f30aa9e…", true, ""]</span>205<span class="send">→ ["EVENT", {"kind":78, "tags":[["d","castcat/history"]],206 "content":"{\"episode\":\"s02e04\",\"finished_at\":1749762900}", …}]</span>207<span class="recv">← ["OK", "e44d1b90cc…", true, ""]</span>208209<span class="c">-- device 2: restore state --</span>210<span class="send">→ ["REQ", "cfg", {"kinds":[30078], "authors":["22a12a12…"],211 "#d":["castcat/settings"]}]</span>212<span class="recv">← ["EVENT", "cfg", {"kind":30078, "content":"{\"speed\":1.5, …}", …}]</span>213<span class="recv">← ["EOSE", "cfg"]</span>214<span class="send">→ ["REQ", "log", {"kinds":[78], "authors":["22a12a12…"],215 "#d":["castcat/history"]}]</span>216<span class="recv">← ["EVENT", "log", {"kind":78, "content":"{\"episode\":\"s02e04\", …}", …}]</span>217<span class="recv">← ["EOSE", "log"]</span>218<span class="send">→ ["CLOSE", "cfg"]</span>219<span class="send">→ ["CLOSE", "log"]</span></pre>220 </div>221222 <p>When the user changes the playback speed, the app publishes a new kind-30078 event with the same <code>d</code> value. The relay drops the old settings event. The kind-78 history keeps growing, one event per finished episode.</p>223224 <h2 class="bar" id="history"><span class="num">5</span> Version History</h2>225 <p>NIP-78 is a short spec and has barely changed since fiatjaf proposed it. Notable changes, newest first:</p>226 <ul class="patchnotes">227 <li><span class="ver">2026</span><span class="when">2026-05</span>Added kind <code>78</code>, a regular event kind for apps that store and query multiple events of the same type.</li>228 <li class="minor"><span class="ver">2024</span><span class="when">2024-08</span>Renamed "parameterized replaceable event" to "addressable event" in a repo-wide terminology sweep.</li>229 <li class="minor"><span class="ver">2023</span><span class="when">2023</span>Grammar and typo fixes; author lines removed from all NIPs.</li>230 <li><span class="ver">Origin</span><span class="when">2023-02</span>fiatjaf published NIP-78: app-specific data with kind <code>30078</code>.</li>231 </ul>232233 <div class="credits">234 <h3 class="cardhead">Original Proposal - Credits</h3>235 <table>236 <tr><td>Author</td><td>fiatjaf</td></tr>237 <tr><td>First commit</td><td>2023-02-23 — “NIP-78: app-specific data.”</td></tr>238 <tr><td>Status</td><td>draft - optional</td></tr>239 <tr><td>Source</td><td><a href="https://github.com/nostr-protocol/nips/blob/master/78.md">78.md on GitHub</a> - <a href="https://github.com/nostr-protocol/nips/commits/master/78.md">full change history</a></td></tr>240 </table>241 </div>242243 <h2 class="bar"><span class="num">6</span> See Also</h2>244 <ul>245 <li><a href="01.html">NIP-01</a> — defines addressable and regular storage classes, the <code>d</code> tag index, and the <code>#d</code> filter used here.</li>246 <li><a href="44.html">NIP-44</a> — encrypted payloads; use it to keep "personal private data" actually private.</li>247 <li><a href="65.html">NIP-65</a> — relay list metadata; a way to advertise which relays a user writes to.</li>248 <li><a href="42.html">NIP-42</a> — client authentication; useful for a storage relay that restricts reads and writes.</li>249 </ul>250 </div>251252 <aside>253 <div class="cartridge">254 <div class="inner">255 <h3 class="cardhead">Spec Card</h3>256 <div class="lights"><span class="led on"></span><span class="led on"></span><span class="led on"></span></div>257 <dl>258 <dt>Status</dt><dd><span class="chip draft">draft</span></dd>259 <dt>Defines</dt><dd>kind 30078 (addressable slot) - kind 78 (regular collection)</dd>260 <dt>Kinds</dt><dd>78 - 30078</dd>261 <dt>Requires</dt><dd><a href="01.html">NIP-01</a> (events, storage classes, <code>d</code> tag)</dd>262 <dt>Payload</dt><dd><code>content</code> and tags: anything, any format</dd>263 <dt>First commit</dt><dd>2023-02-23</dd>264 <dt>Last updated</dt><dd>2026-05-28</dd>265 </dl>266 </div>267 </div>268 </aside>269 </main>270271 <nav class="navbtns">272 <a href="77.html">◀ NIP-77: Negentropy Syncing</a>273 <a href="7d.html">NIP-7D: Forum Threads ▶</a>274 </nav>275276 <footer class="pagefoot">277 <a href="../index.html">Nostr Protocol Manual</a>278 <span class="pageno">NIP 78 / F4</span>279 </footer>280</div>281282<script src="../assets/webmcp.js" defer></script>283</body>284</html>