<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 3001</title>
    <description>The latest articles on DEV Community by 3001 (@kengirie).</description>
    <link>https://dev.to/kengirie</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3974242%2Fededd86d-e443-41c1-b181-2c9342cdf81b.jpeg</url>
      <title>DEV Community: 3001</title>
      <link>https://dev.to/kengirie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kengirie"/>
    <language>en</language>
    <item>
      <title>I made a censorship-resistant static website viewer with no serving host on Nostr</title>
      <dc:creator>3001</dc:creator>
      <pubDate>Mon, 08 Jun 2026 13:25:57 +0000</pubDate>
      <link>https://dev.to/kengirie/i-made-a-censorship-resistant-static-website-viewer-with-no-serving-host-on-nostr-551k</link>
      <guid>https://dev.to/kengirie/i-made-a-censorship-resistant-static-website-viewer-with-no-serving-host-on-nostr-551k</guid>
      <description>&lt;h2&gt;
  
  
  What I made
&lt;/h2&gt;

&lt;p&gt;I made an Android app called &lt;strong&gt;Bouquet&lt;/strong&gt;. In short, it is a viewer that shows you a website, even though no server is serving that website.&lt;/p&gt;

&lt;p&gt;When you open a normal website, your browser talks to some company's server, and downloads HTML and images from it. The server for &lt;code&gt;example.com&lt;/code&gt; is &lt;code&gt;example.com&lt;/code&gt;'s server. One site has one host. Bouquet does not have this kind of dedicated host for the site.&lt;/p&gt;

&lt;p&gt;It does not mean there is no server at all. The parts of the site are stored on general servers (relays and Blossom, I will explain them later). But these are just shared storage that many people use for many kinds of data. They do not exist to serve one specific site. The work to collect the parts and build them back into one site is done by your phone, not by a server.&lt;/p&gt;

&lt;p&gt;There is no dedicated host in the middle. This is the main point of this project.&lt;/p&gt;

&lt;p&gt;First let me explain how a normal website works, then why we can remove the host.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a normal website works
&lt;/h2&gt;

&lt;p&gt;When you open &lt;code&gt;https://example.com&lt;/code&gt;, this is roughly what happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You  --"give me example.com"----&amp;gt;  example.com's server
You  &amp;lt;---- HTML / CSS / images --  example.com's server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One server holds everything. This has some effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If the server goes down, you cannot see the site.&lt;/li&gt;
&lt;li&gt;The owner of the server can see who watched what, and when.&lt;/li&gt;
&lt;li&gt;If the domain is taken down, the site disappears.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is convenient in daily life, but you are trusting everything to one host. Bouquet tries to remove this single point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The key idea: find files by content, not by location
&lt;/h2&gt;

&lt;p&gt;A normal website finds a file by its &lt;strong&gt;location&lt;/strong&gt;: &lt;code&gt;/index.html&lt;/code&gt; on &lt;code&gt;example.com&lt;/code&gt;. Because you use the location, you need a host that owns the location.&lt;/p&gt;

&lt;p&gt;Bouquet finds a file by its &lt;strong&gt;content&lt;/strong&gt; instead.&lt;/p&gt;

&lt;p&gt;If you put a file into a hash function, you get a short string, like a fingerprint. The same content always makes the same fingerprint. If even one byte changes, the fingerprint becomes completely different. So if you know the fingerprint, you can check by yourself if the file is the one you wanted, no matter who gave it to you.&lt;/p&gt;

&lt;p&gt;This is the important point. If you can find a file by content, the file does not need to be on one specific trusted server. You can get it from anywhere, and if the fingerprint matches, it is the real one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts are in two places
&lt;/h2&gt;

&lt;p&gt;The data of a site is stored in two kinds of place. These are the only two players.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relays have the "file to fingerprint" table
&lt;/h3&gt;

&lt;p&gt;A relay is just a server that keeps short messages and sends them again. Bouquet stores the table of contents of the site on relays. The table is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;/index.html&lt;/span&gt;  &lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="err"&gt;hash&lt;/span&gt; &lt;span class="err"&gt;abc123...&lt;/span&gt;
&lt;span class="err"&gt;/style.css&lt;/span&gt;   &lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="err"&gt;hash&lt;/span&gt; &lt;span class="err"&gt;def456...&lt;/span&gt;
&lt;span class="err"&gt;/logo.png&lt;/span&gt;    &lt;span class="err"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="err"&gt;hash&lt;/span&gt; &lt;span class="err"&gt;789xyz...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is just a "file path to fingerprint" table. The file contents are not here. It is only a map that says "this site is made of these files, and each fingerprint is this."&lt;/p&gt;

&lt;h3&gt;
  
  
  Blossom has the file contents
&lt;/h3&gt;

&lt;p&gt;Blossom is a storage that you read and write by fingerprint. If you say "give me the file with hash &lt;code&gt;abc123...&lt;/code&gt;", it returns the bytes.&lt;/p&gt;

&lt;p&gt;Because Blossom manages contents by fingerprint, it does not care who owns the site, or which server it is. You can have many Blossom servers, and from any of them, you can still check the bytes with the fingerprint in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phone resolves everything
&lt;/h2&gt;

&lt;p&gt;Now let me put them together. This is what Bouquet does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get the table of contents (path to fingerprint) from a relay.&lt;/li&gt;
&lt;li&gt;Use the fingerprints to get the file contents from Blossom.&lt;/li&gt;
&lt;li&gt;Hash each file by yourself, and check it with the table.&lt;/li&gt;
&lt;li&gt;When everything is ready, build and show the site, all on the phone.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The phone does all steps by itself. No host that "serves the site" appears anywhere. The relay only gives the table, and Blossom only gives the bytes. They do not show the site. The site is built on the device at the end.&lt;/p&gt;

&lt;p&gt;On a normal website, the server does this work for you: read the manifest, collect the files, and build the page. Bouquet just moves this work to the phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Actually, all of this is Nostr
&lt;/h2&gt;

&lt;p&gt;I have used the word "relay" without explaining it. Relays come from &lt;strong&gt;Nostr&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you do not know it: Nostr is a decentralized protocol for social apps. Instead of a domain or a company account, your identity is a key that you own. Your public key is your name. What &lt;code&gt;example.com&lt;/code&gt; is for a normal website, a public key is for Nostr. The messages you sign with your key are stored and re-sent by relays, and anyone can run a relay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Blossom&lt;/strong&gt; is the file (media) storage that is often used with Nostr. It is the content-addressed storage I explained above: you give a hash, and you get the bytes.&lt;/p&gt;

&lt;p&gt;In Bouquet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The owner of the site is one public key (the &lt;code&gt;npub1…&lt;/code&gt; string).&lt;/li&gt;
&lt;li&gt;The table of contents is a message signed by that key, stored on relays.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because of the signature, you can check that the table really comes from the owner, and was not swapped on the way. The contents are checked with the fingerprints in the table. So from the start to the end, you can verify everything by yourself, without trusting any host in the middle.&lt;/p&gt;

&lt;h3&gt;
  
  
  About NIP-5A
&lt;/h3&gt;

&lt;p&gt;The data format (how a static site is placed on relays and Blossom) is defined by a spec called &lt;a href="https://github.com/nostr-protocol/nips/blob/master/5A.md" rel="noopener noreferrer"&gt;NIP-5A&lt;/a&gt;: the path-to-hash table, how the file contents are stored, and so on. Bouquet uses this format as it is, and does not add any custom event kinds or tags. The data it publishes is normal NIP-5A.&lt;/p&gt;

&lt;p&gt;But here is one important thing. NIP-5A only describes one way to turn the data back into a site: run a host server and resolve it there. The rendering model that the spec assumes is the traditional one, with a smart host in the middle.&lt;/p&gt;

&lt;p&gt;The main idea of this article (no host in the middle, and getting the table, getting the bytes, checking hashes, and building the site, all on the phone) is not written in the NIP-5A text. Bouquet moves this resolution step to the client itself, using only the data that the spec already defines.&lt;/p&gt;

&lt;p&gt;So to be exact, Bouquet is not really "a NIP-5A viewer." It uses NIP-5A data as it is, but it replaces the host-based resolution (which the spec treats as standard) with host-less, client-side resolution. (I also proposed to add this as a section to the spec.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How it actually renders
&lt;/h2&gt;

&lt;p&gt;If there is no host in the path, what is the WebView loading? It is a small local server that the app runs inside the phone, only during the session.&lt;/p&gt;

&lt;p&gt;After the files are collected and verified, Bouquet runs a small server on &lt;code&gt;http://127.0.0.1&lt;/code&gt; (the device talking to itself), and points the WebView to it. &lt;code&gt;127.0.0.1&lt;/code&gt; never leaves the device, so there is no external host here either. When you close the site, the server also goes away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get from this
&lt;/h2&gt;

&lt;p&gt;When you remove the dedicated serving host, you get two concrete things. Both come from the same fact: relays and Blossom servers can be many, the table is verified by signature, and the contents are verified by hash.&lt;/p&gt;

&lt;h3&gt;
  
  
  It is hard to take down
&lt;/h3&gt;

&lt;p&gt;On a normal website, if &lt;code&gt;example.com&lt;/code&gt;'s server goes down, you cannot see the site. That one server is a single point of failure.&lt;/p&gt;

&lt;p&gt;A site through Bouquet does not have that one server. The table can be on several relays, and the contents on several Blossom servers. If one relay goes down, you can get the same table from another. If one Blossom goes down, bytes from another Blossom are fine, as long as the hash matches. There is no single box that ends the site when it dies. The site stays visible as long as one relay still has the table, and one Blossom still has each file.&lt;/p&gt;

&lt;h3&gt;
  
  
  It is strong against censorship and tampering
&lt;/h3&gt;

&lt;p&gt;When there is a dedicated host, that host is the weak point. Push the operator, take the domain, or drop the subdomain. Any one of these can take the whole site offline. A host in the middle is a point that someone can control.&lt;/p&gt;

&lt;p&gt;Bouquet does not have such a point. Everything is spread over many relays and Blossom servers, and anyone can start a new one. If you block one, people just get it from another route.&lt;/p&gt;

&lt;p&gt;And because nothing is in the middle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nobody can swap the content.&lt;/strong&gt; The table is signed by the owner, and every file is checked by hash. So if someone tampers with the data on the way, the phone notices it and rejects it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is harder to watch you.&lt;/strong&gt; No single party sees your whole session. A relay you ask can know which site's table you wanted, and a Blossom you ask can know which file you got. But no single party sees the full picture.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The source and a demo are on GitHub:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/kengirie/bouquet-android" rel="noopener noreferrer"&gt;https://github.com/kengirie/bouquet-android&lt;/a&gt;&lt;/p&gt;

</description>
      <category>nostr</category>
      <category>android</category>
      <category>kotlin</category>
      <category>decentralization</category>
    </item>
  </channel>
</rss>
