<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="/assets/feed-content.xsl?v=d32d9b4191"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
<channel>
    <title>Pablo Murad</title>
    <link>https://pablomurad.com</link>
    <atom:link href="https://pablomurad.com/feed-full/" rel="self" type="application/rss+xml" />
    <atom:link href="https://websubhub.com/hub" rel="hub" />
    <description>I build things that put education within reach and keep the web open.</description>
    <language>en</language>
    <generator>pablawn-v1</generator>
    
    <item>
        <title>No Gods, No Kings</title>
        <link>https://pablomurad.com/no-gods-no-kings/</link>
        <guid isPermaLink="true">https://pablomurad.com/no-gods-no-kings/</guid>
        <pubDate>Mon, 10 Aug 2026 05:48:28 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>reflection</category>
        <description>A late-night reflection on religion, myth, and the Pirahã people of the Brazilian Amazon, a culture often described as non-theistic, focused on direct experience, with no creation myth, supreme god, or afterlife mythology.</description>
        <content:encoded><![CDATA[<p>The other day, I was watching podcasts featuring the Brazilian writer Jan Val Ellam (the pseudonym of Rogério de Almeida Freitas). I like him a lot, despite not believing in the spiritual and <strong><em>cosmicist </em></strong>theories he explains in the form of “spiritual gossip.” However, Jan seems very lucid and confident about what he says, which makes me pay attention, even though I disagree with many things, as I have already said.</p><p>He always brings up very interesting theological and technological reflections that, in the end, would make excellent fiction books.</p><p>But I cannot deny it: reality, first, is shaped in fiction.</p><p>I am not going to go deeply into the general points discussed by Jan, but rather into factual things that are widely known throughout history: the theology of religions.</p><p>Jan claims to have studied more than 100 religions in depth, and while that is curious, it is neither impossible nor unlikely. The knowledge he expresses can be compared with and verified through the literature.</p><p>He even makes a very interesting connection between religions, stating that almost all of them have some form of the trinity. Interesting, right? And throughout the conversations, he keeps showing this. How humans recognize God and how the trinity makes itself present here and there, sometimes indirectly. And it is not even because of syncretism.</p><p>The fact is, one word caught my attention, and it was the word “all” in the phrase “all religions.”</p><p>So I began researching Western and Eastern religions, with the intention of finding one in which deities, gods, or supernatural influences in general do not exist.</p><p>And look, none of what I researched or studied was in-depth, but I started from what seemed to me to be the most sensible beginning, which was the religion of ancient Egypt (and yes, I know it is not the first documented religion).</p><p><em>Hehe, I should mention that it was during this research that I discovered the existence of two cities called Thebes. One in Egypt and another in Greece. And I discovered that the myth of Oedipus and the Sphinx is Greek, even though the Sphinx is Egyptian. Pretty crazy, isn’t it?</em></p><p>Anyway, setting aside my pauses for reflections and daydreams, I surprisingly found this culture without deities in Brazil: the Pirahã people, located in the Brazilian Amazon. These people were studied extensively by the linguist and anthropologist Daniel Everett.</p><p>The Pirahã people are widely cited as perhaps the only non-theistic culture in the world. Look how interesting: they have no creation myth, they do not believe in any supreme or creator god, they do not pray, and they have no religious hierarchy. They believe only in what they can see or in accounts from people who witnessed something directly.</p><p>Then I wondered how these people viewed death, and the answer was even more interesting: <em>“the Pirahã people are strictly focused on empirical experience and the present; they have no mythologies about heaven, hell, or reincarnation.”</em></p><p>So when a person dies in the village, death is pragmatically accepted as a biological fact of the natural world.</p><p>The Pirahã people have <strong>NO</strong> kind of myth whatsoever.</p><p>It is a culture shaped through direct testimony.</p><p>I think modern human communication would be more effective if it were based on the Pirahã people.</p><p>Anyway, <em>just a late-night reflection</em>.</p><h3 id="some-random-data">Some random data:</h3><p><em>* Cosmicism, in the Lovecraftian sense, is basically the idea that the universe is so vast, ancient and indifferent that humanity means absolutely nothing to it. There is no promise that reality was made for us, no guarantee that it is understandable, and certainly no reason to believe that whatever exists beyond our little corner of existence would care about us. In Lovecraft, the horror often comes from discovering too much, because the closer a person gets to understanding what reality really is, the more fragile human ideas like importance, order, morality and even sanity begin to look.</em></p><p>The Pirahã people have an estimated population of between <strong>800 </strong>and <strong>900 </strong>people. They live in villages located mainly along the Maici and Marmelos rivers, in the state of Amazonas, Brazil.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://pablomurad.com/content/images/2026/08/maprio.png" class="kg-image" alt="" loading="lazy" width="551" height="556"><figcaption><span style="white-space: pre-wrap;">Map showing the location of the Pirahã people.</span></figcaption></figure><p>That's it. Thank you.</p>]]></content:encoded>
    </item>
    <item>
        <title>a movie to remember: Empire Records</title>
        <link>https://pablomurad.com/a-movie-to-remember-empire/</link>
        <guid isPermaLink="true">https://pablomurad.com/a-movie-to-remember-empire/</guid>
        <pubDate>Mon, 10 Aug 2026 04:28:58 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>movies</category>
        <description>Empire Records still gives me that sense of belonging I felt as a teenager, a nostalgic reminder of when record stores were refuges, music shaped our identities, and discovering a new song could feel magical.</description>
        <content:encoded><![CDATA[<p>I remember when I first watched <em>Empire Records</em>. It was sometime around the year 2000. I was still a teenager, and it had probably been about five years since its release. I also remember the feeling the film left me with: a sense of belonging.</p><p>Although it is technically a simple story in which a group of young people spend a day trying to save the small record store where they work, while each of them deals with their personal dramas, insecurities, and a few romances, what makes the film special is not the plot. It is the atmosphere.</p><p>For them, the place was not just a store. It was their meeting place and their refuge. It basically made them a family, surrounded by countless CDs and posters.</p><p>Nowadays, rewatching it makes me feel somewhat nostalgic, because I am forced to remember a time when, in order to discover music, you had to visit a record store, and that was, in fact, magical. I would also like to give an honorable mention here to the “Disco-briu” record store in my hometown, which possibly closed around the same time I first watched <em>Empire Records</em>. But the film captures a feeling shared by everyone who lived through part of the 1990s: the intimate relationship between music and identity, combined with youth.</p><blockquote><em>Damn the man. Save the Empire.</em></blockquote>]]></content:encoded>
    </item>
    <item>
        <title>Ghost CMS on Debian</title>
        <link>https://pablomurad.com/ghost-cms-on-debian/</link>
        <guid isPermaLink="true">https://pablomurad.com/ghost-cms-on-debian/</guid>
        <pubDate>Sun, 09 Aug 2026 07:54:59 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>debian</category>
        <description>A practical guide to running Ghost 6 on Debian 13 despite its official Ubuntu-only support, using Node 22, Percona MySQL 8, systemd, CloudPanel, nginx, and a few careful workarounds.</description>
        <content:encoded><![CDATA[<p>I haven't had much time to post. A lot has been happening in my life: losses, goodbyes, and changes (something I hate). But this morning, when I sat down in front of the PC and saw that my server had run into a small problem that prevented access to some of my sites, I fixed it right away and brought all the services back up. The last one was this site, which I stared at for about 20 minutes, frozen.</p>
<p>I've had a few websites built 100% by hand (and almost all of them were on this domain), and I loved it. I've always been terrible at layouts and design, which is good for me because I was constantly changing everything here on the site. It never stopped and eventually became exhausting. From complex to minimalist.</p>
<p>But this year, specifically this year, along with everything life threw at me, work consumed me almost completely. With that said, I needed to move to something faster to update, and since I didn't want WordPress, I chose Ghost.</p>
<p>I'd used Ghost before and had always liked it. I even tried other frameworks, but Ghost was by far the one that gave me the most satisfaction.</p>
<p>But now I had a different little problem when it came to getting this framework running, and that problem was Debian. I knew the documentation and knew Ghost had been designed for Ubuntu, but in theory it would run on Debian with one or two workarounds.</p>
<p>Everyone who has installed Ghost with <code>ghost-cli</code> knows the first frustration: the official tool only embraces Ubuntu LTS. Run <code>ghost install</code> on Debian and it knocks on the door, performs a stack check, sees <code>ID=debian</code>, and simply refuses to continue. The message is clear:</p>
<blockquote>
<p>Unsupported system.</p>
</blockquote>
<p>But "unsupported" doesn't mean "impossible." At its core, Ghost is a Node app with three real requirements:</p>
<ul>
<li>Node 22</li>
<li>MySQL 8</li>
<li>A process manager, with the app served behind a reverse proxy with TLS</li>
</ul>
<p>None of that is exclusive to Ubuntu.</p>
<p><code>ghost-cli</code> only assumes Ubuntu so it can automate those pieces. If you provide each one yourself, using the proper Debian equivalent, Ghost runs smoothly.</p>
<p>That's exactly what I did on my server, running Debian 13 "Trixie." Here's the recipe for this little achievement.</p>
<h2 id="table-of-contents">Table of Contents</h2>
<ol>
<li><a href="#1-disable-the-gatekeeper">Disable the Gatekeeper</a></li>
<li><a href="#2-node-22-from-the-right-source">Node 22 from the Right Source</a></li>
<li><a href="#3-mysql-8-via-percona">MySQL 8 via Percona</a></li>
<li><a href="#4-systemd-is-the-part-that-just-works">systemd Is the Part That Just Works</a></li>
<li><a href="#5-the-split-that-brings-everything-together">The Split That Brings Everything Together</a></li>
</ol>
<h2 id="1-disable-the-gatekeeper">1. Disable the Gatekeeper</h2>
<p><code>ghost-cli</code> v1.29.3 refuses anything that isn't Ubuntu during the stack check. The key is to bypass that check and install Ghost without letting it block the process because of the operating system, while taking responsibility for the dependencies myself. From that point on, it treats the installation like a normal production instance: versioning, systemd, updates, everything.</p>
<h2 id="2-node-22-from-the-right-source">2. Node 22 from the Right Source</h2>
<p>Ghost 6 requires Node 22, and the Debian repository doesn't reliably provide that version. The clean solution is the official NodeSource repository at <code>deb.nodesource.com/node_22.x</code>. This makes <code>/usr/bin/node</code> exactly the 22.x version Ghost requires, with no hack and no need to compile anything.</p>
<h2 id="3-mysql-8-via-percona">3. MySQL 8 via Percona</h2>
<p>This is the obstacle that defeats most people. Ghost 6 requires MySQL 8 and refuses MariaDB, while Debian offers MariaDB by default. Installing Oracle's MySQL on Debian is annoying.</p>
<p>The elegant way out was Percona Server 8.0, a drop-in server that is 100% protocol-compatible with MySQL 8.</p>
<h2 id="4-systemd-is-the-part-that-just-works">4. systemd Is the Part That Just Works</h2>
<p>This one came for free. <code>ghost setup systemd</code> generates a service with <code>User=ghost</code>, <code>ExecStart=node ghost run</code>, and <code>Restart=always</code>. Since Debian's systemd is identical to Ubuntu's, the service came up verbatim, without a single line of adjustment. It's proof that much of the "Ubuntu-only support" is more convention than technical necessity.</p>
<h2 id="5-the-split-that-brings-everything-together">5. The Split That Brings Everything Together</h2>
<p>The trick that makes all of this solid is not letting <code>ghost-cli</code> handle the edge. Nginx and TLS are used the way they always have been: nginx handles the reverse proxy and terminates HTTPS with a Let's Encrypt certificate, forwarding requests to Ghost at <code>127.0.0.1:2589</code>. <code>ghost-cli</code> handles only the portable parts, the Ghost application and systemd.</p>
<p>The result is a clean split:</p>
<ul>
<li><strong>CloudPanel:</strong> nginx, TLS, and Percona/MySQL 8, the "Ubuntu-specific" part that causes trouble in <code>ghost-cli</code></li>
<li><strong>ghost-cli:</strong> the Ghost app and the systemd service, the OS-agnostic part</li>
</ul>
<p>In the end, Ghost's "Ubuntu-only support" is, in practice, support for the automation, not an engineering constraint. When you break the requirement down into its real parts, each one has a legitimate replacement on Debian. The final trick is remembering to pass the stack-check bypass during updates with <code>ghost update</code>, and never letting <code>ghost-cli</code> try to touch nginx or TLS.</p>
<p>I think that's basically it, if memory serves.</p>
<p>Sorry for disappearing. I'm sorting out my life, and I'll get back to posting more.</p>
]]></content:encoded>
    </item>
    <item>
        <title>a new project is on the way</title>
        <link>https://pablomurad.com/a-new-project-is-on-the-way/</link>
        <guid isPermaLink="true">https://pablomurad.com/a-new-project-is-on-the-way/</guid>
        <pubDate>Thu, 30 Jul 2026 12:35:12 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>News</category>
        <description>After a few very difficult weeks, a new project idea lifted my spirits last night. It may not be innovative, but it could be useful to the IndieWeb, and I’m excited by what I’ll learn while building something that helps others.</description>
        <content:encoded><![CDATA[<p>Two years ago, I worked as a director at Genmap, in a kind of interim position that combined the roles of company director and project director. It was one of the best experiences I've ever had. As someone who programs as a hobby, I learned a lot from the company's professionals.</p><p>And I learned. I learned a lot about crawlers.</p><p>After all, Genmap is a Brazilian company that specializes in sitemaps.</p><p>That time at the company was truly incredible. We built so many engines and bots that I lost count, with new features every day, all designed to find as many links as possible on a website (after all, that's what a sitemap does).</p><p>I learned about crawl depth, crawling, concurrency, and other incredible related topics that I'll talk about another time.</p><h1 id="the-idea">The Idea</h1><p>Last night, I called a former coworker. We stayed on a video call for almost an hour because I couldn't get an idea out of my head.</p><p>The idea was to build a crawler for the IndieWeb.</p><p>No admin panel, dashboard, or anything else. I would use only one or two seed URLs as starting points, then let it crawl as much as it could handle.</p><p>My questions for my former coworker were things like:</p><p>•&nbsp;&nbsp;&nbsp;&nbsp; What's the logic behind search engines?</p><p>•&nbsp;&nbsp;&nbsp;&nbsp; How much hardware would a given task require?</p><p>•&nbsp;&nbsp;&nbsp;&nbsp; What about bot blocking?</p><p>•&nbsp;&nbsp;&nbsp;&nbsp; What about robots.txt?</p><p>And many others.</p><p>There really were many more questions. I needed to cut the conversation short before it ran all night.</p><h1 id="reverse-scope">Reverse Scope</h1><p>Well, as with any other project, I start by writing down what I don't want it to be. I write down everything my project should stay away from. What remains is what it should be, what it could be, or what it could grow into.</p><p>It's more or less like saying, "This water won't be coffee..." You can understand that as, "Then it could be any kind of juice."</p><p>This way of thinking has always kept me optimistic about everything I've done in life, because if I knew where not to go, every other path was an adventure and any result was satisfying.</p><p>I had made a decision. I needed to find experienced developers to build an engine for the IndieWeb. That would take money, time, and attention. And, of course, every bit of help I could get.</p><p>From 10:00 p.m. until a little after 11:00, I thought about and wrote down everything my project shouldn't be. And look, I had the perfect <strong>reverse scope</strong>, hehe.</p><h1 id="finding-the-people">Finding the People</h1><p>I joined my friends in the Portal IDEA channel on our private IRC network and dropped the bomb. In less than an hour, I had about four people interested. They seemed genuinely interested because we were doing it for the love of it and to learn.</p><p>I offered to cover the infrastructure. They would take care of the code.</p><p>After a few very complicated weeks in my life, I was actually pretty excited to help make this project happen.</p><h1 id="now-we-wait">Now We Wait</h1><p>Now we'll wait a few months and see what happens...</p>]]></content:encoded>
    </item>
    <item>
        <title>The Wayseer Manifesto</title>
        <link>https://pablomurad.com/the-wayseer-manifesto/</link>
        <guid isPermaLink="true">https://pablomurad.com/the-wayseer-manifesto/</guid>
        <pubDate>Wed, 29 Jul 2026 15:31:25 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        
        <description>Attention, all you rule breakers, misfits, and troublemakers. All you free spirits and pioneers, all you visionaries and nonconformists. Everything the establishment told you was wrong with you is exactly what is right with you. You see things other people don&#39;t. You&#39;re wired to change the</description>
        <content:encoded><![CDATA[<p><strong>Attention, all you rule breakers, misfits, and troublemakers.</strong> All you free spirits and pioneers, all you visionaries and nonconformists. Everything the establishment told you was wrong with you is exactly what is right with you. You see things other people don't. You're wired to change the world. Unlike nine out of ten people, your mind can't be repressed, and that threatens authority. You were born to be a revolutionary. You can't stand rules because, in your heart, you know there is a better way. You have strengths the establishment considers dangerous, and it wants them eliminated. So, your entire life, you've been told that your strengths were weaknesses.</p>
<p>Jobs</p>
<h2 id="im-telling-you-the-opposite">I'm telling you the opposite.</h2>
<p>Your impulsiveness is a gift. Impulses are your key to the miraculous. Your distractibility is evidence of your inspired creativity. Your mood swings follow the natural pulse of life, giving you unstoppable energy when you're high, then deep and moving insight when you're low. Labeling you with a disorder is society's newest way of denying its own sickness by pointing a finger at you. Your addictive personality is a symptom of your vast, underused capacity for heroic creative expression and spiritual connection.</p>
<p><strong>Your complete lack of inhibition. Your wide-eyed idealism. Your utterly open mind.</strong></p>
<p>Has no one ever told you? These are the strengths shared by the greatest pioneers and visionaries. Innovators, revolutionaries, procrastinators, drama queens, social activists, daydreamers, nonconformists, philosophers, outcasts, people in suits and ties, football stars and sex addicts, celebrities with ADD, novelty-seeking alcoholics, first responders, prophets and saints, mystics and agents of change.</p>
<blockquote>
<p><em>We're all the same, you know. Because we're all touched by the wave.</em></p>
<p><em>We're all the same, you know. Because we're all drawn to the flame.</em></p>
</blockquote>
<hr>
<p>You know in your heart that there is a natural order. Something more sovereign than any rule or law made by human beings could ever express.</p>
<h2 id="that-natural-order-is-called-the-way">That natural order is called the Way.</h2>
<p><strong>The Way is the eternal substrate of the cosmos.</strong> It guides the joyful currents of time and space. Some know it as the will of God, divine providence, the Holy Spirit, the implicate order, the Tao, reverse entropy, the life force. For now, let's simply call it the Way.</p>
<p>The Way is reflected in you as the source of your inspiration, your passions, your wisdom, your enthusiasm, your intuition, your spiritual fire, your love. The Way takes the chaos of the universe and breathes life into it, giving it divine order. When expressed through the mind, the Way is genius. When perceived by the eyes, it is beauty. When felt through the senses, it is grace. When allowed into the heart, it is love.</p>
<hr>
<h2 id="most-people-cant-perceive-the-way-directly">Most people can't perceive the Way directly.</h2>
<p>But then there are the wayseers, the keepers of the flame.</p>
<p>Wayseers have an inexplicable gift for simply knowing the Way. They feel it in their own being. They can't tell you why or how they arrived at the right answer. They just know it at their core. They can't show you how they got there, so don't ask. Their minds simply resonate with the Way. When the Way is present, so are they. While others are blind to it, and society begs you to ignore it, the Way stirs inside you.</p>
<p><strong>Neurological repression blocks most people's awareness of the Way.</strong> Your prefrontal cortex, the Gestapo of the brain, censors every thought and impulse rising from the unconscious. Nothing that violates its social programming is allowed through.</p>
<h2 id="but-your-mind-is-different">But your mind is different.</h2>
<p>Your mind was thrown wide open to the Way. Through some miraculous genetic trait, some psychotropic chemical, or perhaps even the will of your own soul, your brain's reward pathways were hijacked. Dopamine was recruited to overthrow the fascist dictatorship of your prefrontal cortex. Now your brain is free from repression. Your mind is free from censorship. Your consciousness is exposed to the turbulent seas of the unconscious. Through that open door, divine light shines into your awareness and shows you the Way.</p>
<p><strong>That is what makes you a wayseer.</strong></p>
<hr>
<p>Ninety percent of human civilization is populated by people whose brains are closed to the Way. Their minds are programmed to enforce the social doctrine installed in them from birth. Unlike you, they can't break through that programming because they haven't yet experienced the necessary revolution of the mind.</p>
<p>Programmed people take social institutions and rules very seriously. Society is packed with games designed to keep people's minds occupied so they won't revolt. These games often create unhealthy fixations on peculiar protocols, power structures, taboos, and domination. They are all subtle forms of human slavery.</p>
<h2 id="this-particular-kind-of-madness-is-tolerated-by-the-masses">This particular kind of madness is tolerated by the masses.</h2>
<p><strong>It is demanded.</strong></p>
<p>The programmed believe in the rules so fiercely that they become willing to destroy anyone who breaks them. Wayseers are the ones who expose them.</p>
<p>Because their minds are free to reject social programming, wayseers can see these institutions for what they are: imaginary games. Wayseers comfort the disturbed and disturb the comfortable. Helping those who are lost inside these games, even when they refuse to help themselves, is the calling of many wayseers.</p>
<p>Wayseers remain in contact with the original source of reality. That is why they can disrupt social conventions and even governments, forcing humanity back into alignment with the Way. They are an ancient lineage, a kind of priesthood, bearers of the flame, the ones who know.</p>
<p>There must always be wayseers to reform the dizzying, psychotic machinery of society, those gigantic and mindless hamster wheels that blot out the clear blue sky while keeping humanity chained inside a darkened cage.</p>
<p><strong>And so wayseers are called to shine a light on society's madness, continually bringing the timeless, transcendent spirit of truth back to life.</strong></p>
<p>Wayseers reveal that divine truth by giving themselves to the birth of something creative or disruptive: art and philosophy, innovations that shake industries, revolutions for democracy, blows against hypocrisy, solidarity movements, changes that leave a legacy, rebellions against policy, technology filled with spirit, moments of clarity, things that challenge barbarism, milestones of sincerity, monumental acts of charity.</p>
<blockquote>
<p><em>We're all the same, you know, because we're all touched by the Way.</em></p>
<p><em>We're all the same, you know, because we're all drawn to the Way.</em></p>
</blockquote>
<hr>
<h2 id="this-is-your-calling-wayseer">This is your calling, wayseer.</h2>
<p>You've found your tribe.</p>
<p><strong>Welcome home.</strong></p>
]]></content:encoded>
    </item>
    <item>
        <title>the dawn of rss feeds</title>
        <link>https://pablomurad.com/the-dawn-of-rss-feeds/</link>
        <guid isPermaLink="true">https://pablomurad.com/the-dawn-of-rss-feeds/</guid>
        <pubDate>Wed, 29 Jul 2026 15:13:47 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>projects</category>
        <description>An open web social reader. It reads feeds, publishes feeds, and threads conversations over plain RSS...</description>
        <content:encoded><![CDATA[<p>The web had a simple promise: any site could publish a feed, anyone could subscribe, and nobody in the middle got to decide what you saw. RSS made that work for twenty years. What it never had was the <strong>conversation</strong>. You read someone's post in your reader, but replying means going to that person's platform. Suddenly you're back inside the walled garden RSS was meant to avoid.</p>
<p>Social networks solved the conversation and threw away the rest. The feed belongs to someone else. An algorithm decides the order. Your identity is a username the platform lends you and can take back. Leaving one network means starting from nothing on another, because what you wrote and the people who followed you stayed behind.</p>
<p>RSS Expert wants both things at once: the openness of RSS and the conversation of social networks, <strong>without</strong> an owner standing in the middle. You read feeds as usual. When you reply, that reply is published <strong>in your own feed</strong>, with a link to the original post. Nothing is written on somebody else's server. The other side discovers the reply because it's in the feed and because the system sends a Webmention.</p>
<p>The conversation happens, but it remains yours, at your address.</p>
<h2 id="what-it-is">What it is</h2>
<p>One static binary, one SQLite file, one container.</p>
<p>The only JavaScript is a twenty-line island that reveals a "new post" notice. Every page works without it. There's no CSS framework, no build step for the assets, and no separate database server to bring up. That's deliberate. One person should be able to run the whole thing on a small server and understand all of it.</p>
<p>These ideas hold up everything else.</p>
<h3 id="provenance-is-first-class-information">Provenance is first-class information</h3>
<p>Most systems store "this post exists." This one stores "this post was seen in this form, from this source, at this time, and I chose this version for this reason."</p>
<p>A post arriving through two paths isn't overwritten. Both observations remain recorded, and convergence chooses a winner deterministically while showing why. The timeline separates what was written here from what arrived from elsewhere, and it never forgets where something came from.</p>
<p>The <em>Where it went</em> screen shows every feed carrying one of your posts and every notification that was sent, including the ones that failed.</p>
<h3 id="your-domain-is-your-identity">Your domain is your identity</h3>
<p>You prove that a site belongs to you using <code>rel=me</code> and an h-card verified in both directions. It becomes your name in the system.</p>
<p>It isn't an account somebody lends you.</p>
<h3 id="federation-isnt-optional">Federation isn't optional</h3>
<p>It's the point. Being open and alone isn't very useful. The system speaks the protocols other open networks already speak instead of inventing its own.</p>
<h2 id="who-it-talks-to">Who it talks to</h2>
<p>On the RSS and IndieWeb side, everything works without anybody else having to run this same program:</p>
<ul>
<li>It reads RSS 2.0, Atom, JSON Feed, h-feed, and OPML.</li>
<li>It publishes a per-user feed, a site-wide feed, and a reply feed for each post. Any reader can subscribe.</li>
<li><strong>WebSub and rssCloud work in both directions.</strong> As a subscriber, RSS Expert discovers a feed's hub or cloud and receives posts as soon as they're published. As a publisher, <strong>it is its own hub and its own cloud</strong>, with no third-party service in between.</li>
<li><strong>Webmention</strong> tells the other side about a reply, and <strong>Micropub</strong> lets another application publish through it.</li>
</ul>
<p>On the fediverse side, enabled with <code>RSS_EXPERT_ACTIVITYPUB</code>:</p>
<ul>
<li><code>@yourhandle@rss.expert</code> becomes an address anyone on Mastodon can find and follow. When you publish, the post appears in their timeline.</li>
<li>Conversation crosses in both directions. A reply written on Mastodon joins the thread here, and a reply written here reaches the remote author's inbox.</li>
<li>Editing sends an <em>Update</em>. Removing a post sends a <em>Delete</em>. Likes and boosts are counted. HTTP signatures support both the newer standard, RFC 9421, and the older draft Mastodon still uses. The system remembers which one each server accepts.</li>
</ul>
<h2 id="what-its-for-and-who-its-for">What it's for, and who it's for</h2>
<p>It's for someone who wants to read the open web in one place, publish without depending on a platform, and talk to people on other networks while keeping their identity, history, and provenance on their own side.</p>
<p>A blog that is also a reader and a fediverse account, without becoming three different services.</p>
<p>It isn't for everyone. If you only want to post something and be seen by a lot of people quickly, a large network will serve you better. RSS Expert trades reach for ownership. Fewer people will find you for free, but what's yours stays yours, and nobody in the middle decides what happens to it.</p>
<h2 id="why-its-built-this-way">Why it's built this way</h2>
<p>Every major choice has a recorded reason in <code>docs/decisions/</code>, in the ADRs. These are the big ones:</p>
<ul>
<li><strong>Small on purpose.</strong> A static binary without cgo, a <code>FROM scratch</code> image, and SQLite instead of a database server. It can run on almost nothing, and there are fewer things waiting to break.</li>
<li><strong>No outside library for the sensitive parts.</strong> ActivityPub HTTP signatures were written by hand and tested against known vectors instead of inheriting an abandoned dependency. TOTP follows the same discipline.</li>
<li><strong>Security defaults to the safe choice.</strong> Every outbound request passes through an SSRF guard, checked after DNS resolution and before connecting, on every hop. Upload types come from their bytes, files are decoded before storage, and metadata is stripped. When a setting is a security decision, its default is the safe one.</li>
<li><strong>It can't look AI-made.</strong> The code reads like a person wrote it. The visual design follows real reference pages, with no purple gradient, glassmorphism, or emoji in the interface. Every color and size comes from one token file.</li>
</ul>
<h2 id="where-the-ideas-came-from">Where the ideas came from</h2>
<p>The implementation is independent. No code was copied from another project.</p>
<p>But the interoperability knowledge that makes it possible came from other people's work: RSS, OPML, rssCloud, and Dave Winer's Textcasting; Ricardo Mendes's RSC, which proved the idea could work and gave this project a map; Micropub, Webmention, and the IndieWeb microformats; and ActivityPub and ActivityStreams from the W3C.</p>
<h2 id="where-it-is">Where it is</h2>
<p><a href="https://rss.expert">rss.expert</a>, version 0.0.1, in testing.</p>
<p>The federation loop has been proven between two instances I control and by a large test suite. One check remains, and no automated test can cover it: a real <code>mastodon.social</code> account following <code>@pablo@rss.expert</code>, with the conversation actually crossing both ways.</p>
<p>Until that happens, "it works" describes the code.</p>
<p>It doesn't yet describe the world outside.</p>
]]></content:encoded>
    </item>
    <item>
        <title>Your Recovery Codes Need a Better Home</title>
        <link>https://pablomurad.com/your-recovery-codes-need-a-better-home/</link>
        <guid isPermaLink="true">https://pablomurad.com/your-recovery-codes-need-a-better-home/</guid>
        <pubDate>Fri, 24 Jul 2026 11:11:40 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>tech</category>
        <description>Recovery codes are useless if they disappear with the device or account they&#39;re meant to recover. Here&#39;s a practical way to store them across paper, encrypted files, and offsite copies without building a system too clever to trust.</description>
        <content:encoded><![CDATA[<p>Imagine a very bad Tuesday.</p>
<p>Your phone is dead. Your laptop was stolen. You try to open your email on another computer, but it asks for a code from the authenticator app that was on the phone. Fine, you think, the recovery codes are in the password manager. Then the password manager asks for the same missing second factor.</p>
<p>The backup file was on the laptop.</p>
<p>Its second copy was in cloud storage connected to the email account you can't open.</p>
<p>Nobody hacked you. You're still locked out.</p>
<p>This is the stupid side of good security. We spend time protecting accounts from strangers and then build a recovery process that depends on every device and service working normally. Of course it looks safe on an ordinary day. Recovery codes exist for the day that isn't ordinary.</p>
<p><img src="../Pesquisadore/pesquisa-040-codigos-recuperacao/recovery-codes-cover.png" alt="A desk showing complementary ways to protect recovery codes: a safe, sealed envelopes, an encrypted digital container, external media, and a spare security key" loading="lazy"></p>
<p>The question seems simple: where should I keep these codes?</p>
<p>The honest answer is more annoying. There isn't one perfect place. You need a few places that fail differently, and the arrangement has to remain understandable when you're tired, worried, using an unfamiliar computer, or explaining it to someone who doesn't care about your beautiful security system.</p>
<p>That last part matters.</p>
<h2 id="recovery-material-is-a-key">Recovery material is a key</h2>
<p>A recovery code isn't a harmless note. Whoever has it may have a way into the account, sometimes with only one other piece of information. Treating it like a receipt in the Downloads folder is strange when you think about what it can do.</p>
<p>Services also use similar names for different things. Google gives you a set of single-use backup codes. Microsoft uses a 25-digit recovery code and replaces the old one when a new code is generated. Apple's recovery key is a 28-character secret with much heavier consequences. If you enable it and later lose the key along with access to a trusted device, Apple says you can be locked out permanently.</p>
<p>Then there are TOTP seeds, Emergency Kits, passkeys, spare security keys, and provider-specific recovery contacts. They don't behave in the same way. A one-time code can be crossed out after use. A TOTP seed can reproduce authenticator codes again and again. A hardware key sitting in a drawer is useless unless it was already registered with the account.</p>
<p>So the first job is identification. Write down what the item actually is, which account it belongs to, when it was created, and whether using it invalidates anything else.</p>
<p>Plain names help. Mysteries don't.</p>
<h2 id="look-for-the-circle">Look for the circle</h2>
<p>Before buying a safe or making an encrypted container, test the dependency.</p>
<p>If the account disappears from your life for one hour, can you still reach its recovery material without using that account?</p>
<p>Google codes stored only in Google Drive fail this test. An Apple recovery key kept only in iCloud Notes fails it too. The Emergency Kit for a password manager can't live exclusively inside that password manager. An encrypted file isn't very impressive when its only passphrase is stored inside the file.</p>
<p>These arrangements can survive for years without showing the problem. That's why they're dangerous. The trap becomes visible at the exact moment you need the exit.</p>
<p><a href="https://support.apple.com/en-us/109345">Apple tells users</a> to print or write down the recovery key, keep it somewhere safe, and consider more than one location. The company specifically warns against leaving the only copy in Apple Passwords, iCloud Photos, Notes, or iCloud Drive. <a href="https://pages.nist.gov/800-63-4/sp800-63b.html">NIST's current authentication guidance</a> also describes saved recovery codes as secrets intended to be kept offline and stored securely.</p>
<p>Paper, apparently, is still alive.</p>
<h2 id="the-paper-envelope-is-boring-and-excellent">The paper envelope is boring and excellent</h2>
<p>Print the critical codes or write them very clearly. Put them in an opaque envelope. Add the date, seal it, sign across the seal, and store it with documents that already deserve protection.</p>
<p>That signature won't stop a determined person. It can tell you that the envelope was opened, which is useful. If the seal looks wrong, enter the accounts through a normal method and regenerate the exposed codes.</p>
<p>The paper inside should be understandable without a tutorial. It needs the service name, account identifier, official recovery address, generation date, number of available codes, primary MFA method, and the location of another valid route. Avoid screenshots when plain text will do. Interfaces age. Text survives.</p>
<p>A small notice on the envelope is enough:</p>
<pre><code class="language-text">EMERGENCY DIGITAL ACCESS
Prepared: YYYY-MM-DD
Review after: YYYY-MM-DD
If this seal is broken unexpectedly, replace every code.
</code></pre>
<p>Don't write every secret you own on the same unprotected page. A password, its TOTP seed, and the recovery codes together can become a complete account takeover kit. Some family or succession plans may need all the pieces, but then the physical protection and the person holding the package become far more serious decisions.</p>
<p>And check the box itself. A thin metal box isn't magically fireproof because a store put the word “safe” on it. Look at its real document rating, including time, temperature, and water protection.</p>
<p>Paper can burn. It can get wet, become outdated, or be photographed. This is why the envelope is a fallback, not the entire plan.</p>
<h2 id="the-encrypted-capsule">The encrypted capsule</h2>
<p><a href="https://alexwlchan.net/2026/recovery-codes/">Alex Chan wrote about</a> keeping his recovery material in a small, encrypted disk image, with offsite backups and a paper copy planned for a fire safe. The useful part of that solution is that the encrypted container can travel while the files inside remain simple.</p>
<p>The container might be a protected disk image, an encrypted virtual disk, or a small <a href="https://veracrypt.io/en/Creating%20New%20Volumes.html">VeraCrypt volume</a>. Inside it, use files that should still open years from now: Markdown, plain text, a self-contained HTML page, perhaps the original PDF supplied by a service.</p>
<p>No custom app. No database that requires an abandoned framework. No personal cipher based on a poem you expect your future self to remember.</p>
<p>Keep the container small and copy it to more than one medium. One copy can stay on the computer, another on disconnected storage, and an encrypted copy can live outside the house. Cloud storage is acceptable for the already encrypted file, provided the passphrase doesn't depend entirely on that same cloud account.</p>
<p>The passphrase needs its own exit. A sealed copy at home can work. A password manager plus an independent physical record can work. Hiding four characters in a photograph from 2009 is how a practical plan turns into folklore.</p>
<p>An encrypted USB drive is still a USB drive. It can fail quietly, disappear in a pocket, or sit unread for years. The encryption protects the contents from whoever finds it. It doesn't make the device immortal.</p>
<h2 id="the-password-manager-problem">The password manager problem</h2>
<p>Keeping ordinary recovery codes in a password manager is convenient. Search works. Updates are easy. It is vastly better than forgetting them in Downloads.</p>
<p>The problem begins when the manager contains the only recovery route for itself, the primary email account, the phone ecosystem, and every other account capable of resetting the rest. What appears to be a collection of separate protections can collapse as one object.</p>
<p>Password managers with emergency features can help. <a href="https://bitwarden.com/help/emergency-access/">Bitwarden Emergency Access</a> lets a designated contact request access after a waiting period. The <a href="https://support.1password.com/emergency-kit/">1Password Emergency Kit</a> contains information needed to set up the account on a new device and is meant to be stored safely.</p>
<p>But a feature nobody configured is decoration.</p>
<p>The contact has to accept. They need to understand when access is allowed, where the instructions are, and what they should never send through chat or email. The password manager itself still needs a route that doesn't depend on opening the password manager.</p>
<h2 id="the-setup-that-makes-sense">The setup that makes sense</h2>
<p><img src="../Pesquisadore/pesquisa-040-codigos-recuperacao/recovery-codes-three-layer.png" alt="A three-location recovery plan with an encrypted capsule, a paper envelope in a home document box, and a separate offsite copy" loading="lazy"></p>
<p>For someone with personal accounts, domains, self-hosted services, and a reasonable tolerance for technical work, a layered setup is the sensible choice.</p>
<p>The working copy is the one available quickly. Ordinary account codes can stay in the password manager. Critical material can also live in the encrypted capsule on the computer, opened only when something needs to be read or changed.</p>
<p>The local physical copy is the sealed envelope in a safe or protected document box. It needs no internet connection, subscription, operating system, or healthy SSD. That's a surprisingly useful list of properties.</p>
<p>The offsite copy lives at another address. It may be another sealed envelope with a trusted person, or an encrypted container whose passphrase travels through a separate route. Another drawer in the same house doesn't count. Neither does a second disk permanently connected to the same machine.</p>
<p>Then add another authenticator where the service allows it. A spare security key is a cleaner first fallback than consuming a recovery code, but only when it was registered in advance. <a href="https://www.yubico.com/products/spare/">Yubico recommends</a> enrolling the spare at the same time as the primary key and storing it somewhere safe and accessible.</p>
<p>This arrangement covers different failures without becoming a small religion. The digital copy handles routine device loss. Paper remains readable after computer trouble. The offsite copy survives the event that takes the whole house. A second authenticator may avoid the recovery process completely.</p>
<p>Neglect can still ruin it. An envelope full of invalid codes is a very organized way to remain locked out.</p>
<h2 id="start-with-the-accounts-that-control-the-others">Start with the accounts that control the others</h2>
<p>Primary email comes first. Then the password manager, phone or operating-system account, domain registrar, cloud storage, code repository, and infrastructure providers. Financial accounts follow their own official recovery rules and deserve the same attention.</p>
<p>These are root accounts. If one of them falls, several others may follow. Protecting a hundred minor logins while the primary email has one fragile recovery route is excellent filing and bad prioritization.</p>
<p>People who run servers have more roots than they think. The domain and DNS provider matter. So do the VPS panel, Tailscale or another administrative network, external backups, the source forge, transactional email, and any control panel that can reset access or destroy machines.</p>
<p>Don't store the only recovery vault on the server it is supposed to help recover. This sentence sounds obvious. Plenty of backup systems contain an equally obvious joke.</p>
<p>Recovery codes don't replace SSH key management, configuration backups, restore notes, or access procedures. Keep those systems connected in the inventory, but don't throw every secret into one convenient package.</p>
<p>A plain inventory is enough:</p>
<pre><code class="language-text">Service:
Account identifier:
Official recovery page:
Primary MFA method:
Registered alternate method:
Recovery material created:
Codes remaining:
Last checked:
Working copy:
Local physical copy:
Offsite copy:
Next review:
Notes:
</code></pre>
<p>The inventory can stay separate from the secrets. Its job is to tell you what exists and where, including which items were deliberately kept elsewhere.</p>
<h2 id="test-while-the-door-is-still-open">Test while the door is still open</h2>
<p>Don't begin with the account that controls your entire digital life.</p>
<p>Choose a low-risk service that provides several recovery codes. Confirm the password and another MFA method first. Open a private browser window, retrieve one code through the route you designed, use it, and mark it as consumed immediately. Then check every copy that contains the individual codes.</p>
<p>For a critical account, you can rehearse without entering a code. Pretend the phone and computer are gone. Can you locate the instructions? Can you obtain the container passphrase? Is the spare key actually registered? Does the official recovery page still exist?</p>
<p><a href="https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/recovering-your-account-if-you-lose-your-2fa-credentials">GitHub warns</a> that support can't restore access when two-factor credentials and every recovery method are gone. If no recovery option works, the account may be permanently lost. That's the sort of policy worth discovering during a test, with a normal session still open.</p>
<p>Review the system after a move, a compromised computer, a broken seal, a changed custodian, or any provider change that replaces old codes. Google and Microsoft invalidate previous sets when new ones are generated, so keeping several generations “in case” creates confusion instead of safety.</p>
<p>An annual check is reasonable for the whole kit. A lighter check every few months makes sense for the root accounts. Weekly maintenance would turn this into a chore, and chores have a way of being abandoned.</p>
<p>The first version doesn't need to be elaborate. Identify the accounts that can recover the others. Generate fresh codes through their official pages. Print and date them. Put one protected copy at home and another at a genuinely different location. Register the spare factor now, while you can still sign in normally.</p>
<p>Then test one unimportant account.</p>
<p>That's enough work for one afternoon, and the result should still make sense on the next bad Tuesday.</p>
<h2 id="sources">Sources</h2>
<ul>
<li><a href="https://alexwlchan.net/2026/recovery-codes/">Alex Chan, Where I store my multi-factor recovery codes</a></li>
<li><a href="https://pages.nist.gov/800-63-4/sp800-63b.html">NIST SP 800-63B, Authentication and Authenticator Management</a></li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Multifactor_Authentication_Cheat_Sheet.html">OWASP, Multifactor Authentication Cheat Sheet</a></li>
<li><a href="https://support.google.com/accounts/answer/1187538">Google, Sign in with backup codes</a></li>
<li><a href="https://support.microsoft.com/en-us/accounts-billing/manage/how-to-get-a-microsoft-account-recovery-code">Microsoft, How to get a Microsoft account recovery code</a></li>
<li><a href="https://support.apple.com/en-us/109345">Apple, Set up a recovery key for your Apple Account</a></li>
<li><a href="https://docs.github.com/en/authentication/securing-your-account-with-two-factor-authentication-2fa/recovering-your-account-if-you-lose-your-2fa-credentials">GitHub, Recovering your account if you lose your 2FA credentials</a></li>
<li><a href="https://support.1password.com/emergency-kit/">1Password, Get to know your Emergency Kit</a></li>
<li><a href="https://bitwarden.com/help/emergency-access/">Bitwarden, Emergency Access</a></li>
<li><a href="https://veracrypt.io/en/Creating%20New%20Volumes.html">VeraCrypt, Creating new volumes</a></li>
<li><a href="https://www.yubico.com/products/spare/">Yubico, Spare YubiKeys</a></li>
</ul>
]]></content:encoded>
    </item>
    <item>
        <title>RSS Expert</title>
        <link>https://pablomurad.com/rss-expert/</link>
        <guid isPermaLink="true">https://pablomurad.com/rss-expert/</guid>
        <pubDate>Fri, 24 Jul 2026 03:37:48 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>projects</category>
        <description>RSS Expert turns RSS into a place for conversation: read, publish, and reply through open feeds, without a central platform. Your identity is your domain, and your audience follows you—not a company.</description>
        <content:encoded><![CDATA[<p>Well, I didn’t know whether something like this already existed. But a while ago, I had an idea: <em>what if I could use RSS feeds to communicate?</em> Everyone knows RSS feeds are one of the basic ways we stay updated, get notifications, and keep ideas moving. So I don’t think I need to explain what they are here.</p><p>But I do want to make a few observations. I still remember the first time I heard about RSS: it was around the same time I joined Reddit, on the day Aaron Swartz died. I remember seeing the news, although I hadn’t yet looked very deeply into his work.</p><p>People were mistakenly saying that Aaron was the creator of Reddit, and that was what drew me there, even though it’s well known that Huffman and Ohanian were its founders. Still, that doesn’t change the fact that he was one of Reddit’s earliest developers and is often treated as a co-founder. It’s also worth noting that he wasn’t involved in the project’s original idea or conception.</p><p>In any case, I started reading about Swartz and found someone with very clear goals and a concrete vision of an open, free internet. Among the works he co-authored, one in particular caught my attention: <strong>RSS</strong>, specifically <strong>RSS 1.0</strong>, whose working group he joined when he was still very young. And from that moment, tied to the life and death of Aaron Swartz, my small obsession with RSS feeds began.</p><p>In a way, almost everything around social networks follows principles that RSS feeds had already established: scrolling, getting notified, receiving updates in real time. Think about it: Facebook and Instagram are, in some sense, megalomaniacal RSS feeds. Someone updates a status; you, through the app, have the aggregator; then you see it and interact with it—just on a much larger scale.</p><p>As I’ve said before in other posts, the <em>fediverse</em> and the <em>pubs</em> became objects of fascination for me in mid-2025, when I spent most of my free time studying this whole development—its nuances, interpretations, and connections. In fact, it’s still a fascination of mine, and it remains incredibly captivating: <strong>decentralization as a form of expression</strong>.</p><p>One night a few months ago, while I was drinking coffee in the lab, a question came to me: <em>what if I ran an experiment to make RSS more communicative?</em></p><p>I didn’t want this experiment to become just one more thing among the hundreds of RSS-related projects sitting in my GitHub. I wanted something relevant. Something more solid.</p><p>So I started researching, and after several analyses, studies, completely failed implementations, and plans that were far too abstract, something finally took shape in my head:</p><p><em>“I’m going to build a feed reader and publisher that lets people talk, with the conversation happening through RSS.”</em></p><p>Then I imagined three things that would normally require three different programs:</p><p>1 – Reading what other people have written <em>(a feed reader)</em></p><p>2 – Writing and being read by the people who follow me <em>(a blog)</em></p><p>3 – Replying to someone and having that reply reach them <em>(a social network)</em></p><p>&nbsp;From that point on, I knew what I had to do: <strong>bring the three together</strong>. I set out to build a system that could do all three things in the same format: <strong>static XML served over HTTP</strong>.</p><p>No API would be needed to take part. If a site published an <em>&lt;item&gt;</em> with a <em>&lt;source:inReplyTo&gt;</em> pointing to one of its posts, that would count as a reply, no matter what software was being used.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://pablomurad.com/content/images/2026/07/image.png" class="kg-image" alt="" loading="lazy" width="716" height="638" srcset="https://pablomurad.com/content/images/size/w600/2026/07/image.png 600w, https://pablomurad.com/content/images/2026/07/image.png 716w"><figcaption><span style="white-space: pre-wrap;">Prototype of RSS Expert</span></figcaption></figure><p>&nbsp;<strong>And why does that matter?</strong></p><p>In a conventional social network, your account belongs to the company. Here, your identity is your domain: you prove that <em>yourwebsite.com</em> is yours, and that’s what appears next to your name. If you leave this instance someday, you take your domain and your readers with you. They follow a feed URL, not a profile.</p><p>Well, I won’t drag this out too much. This is just a glimpse of what I’ve been working on. For now, I think that’s enough...</p>]]></content:encoded>
    </item>
    <item>
        <title>Building text2.site</title>
        <link>https://pablomurad.com/building-text2-site/</link>
        <guid isPermaLink="true">https://pablomurad.com/building-text2-site/</guid>
        <pubDate>Tue, 21 Jul 2026 02:58:39 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>tech</category>
        <description>text2.site strips cluttered web pages down to durable plain text. This is the story of building it with Node, Readability, careful fallbacks, SSRF protection, and a stubborn commitment to simplicity.</description>
        <content:encoded><![CDATA[<p>I opened an article a few days ago. It had maybe 800 words worth reading.</p>
<p>The page downloaded 4 MB.</p>
<p>There was a cookie banner, a newsletter modal waiting for my mouse to move toward the top of the screen, a fixed bar above, another fixed bar below, a "you may also like" box wedged into the middle of a paragraph, three social media embeds, and a video that started playing without being invited. The 800 words were still there, technically. I just had to excavate them.</p>
<p>That was the entire origin story. No revelation. No grand theory about saving the web.</p>
<p>I was annoyed.</p>
<p>So I built <strong>text2.site</strong>, a small service that does one thing: you paste a URL and it gives you the text. No account, no database, no cookies, and nothing stored. Just a <code>.txt</code> file that opens everywhere and will probably continue opening thirty years from now.</p>
<h2 id="why-plain-text">Why plain text</h2>
<p>Plain text may be the most underestimated format we have.</p>
<p>It goes into a note and remains readable. It works in an e-book reader. A screen reader can move through it without tripping over decorative buttons. If the original website disappears, the file does not suddenly forget how to exist. It takes kilobytes instead of megabytes.</p>
<p>More importantly, plain text has no opinion about how you should consume it.</p>
<p>It does not demand a browser, a particular app, an account, JavaScript, or a relationship with an advertising company. It is simply the content, which feels almost radical now.</p>
<h2 id="one-file-no-framework-no-build-step">One file, no framework, no build step</h2>
<p>The first decision was also the most important: use almost nothing.</p>
<p>No React. No Next.js. No bundler, build pipeline, or <code>dist/</code> directory. The service is a Node server using the native <code>http</code> module, with handwritten HTML and CSS, and an <code>npm start</code> command that behaves the same way on my machine and on the VPS.</p>
<p>This is not nostalgia. It is arithmetic.</p>
<p>Every framework I added would become another thing to update, another surface that could break, and another layer between "I want to change this" and "I changed it." For a service with one job, the framework would cost more than it gave me.</p>
<p>The entire front end is three files. The server fits inside my head. I like software that can still fit inside the head of the person maintaining it.</p>
<h2 id="readability-and-what-happens-when-it-gives-up">Readability, and what happens when it gives up</h2>
<p>At the center of the service is Mozilla Readability, the same engine behind Firefox Reader View. It identifies the article and throws away the surrounding machinery. It is mature, has zero dependencies, weighs about 200 KB, and is remarkably good at this specific job.</p>
<p>It also has a personality: when uncertain, it returns nothing.</p>
<p>That makes sense inside a browser. If Firefox is not confident that a page contains an article, it simply does not offer Reader View. You continue reading the normal page. A converter does not have that luxury. If my service says "I found nothing," the user does not get a second option. They leave empty-handed.</p>
<p>So I built a ladder underneath Readability. When the first extraction fails, text2.site tries the following:</p>
<ol>
<li>Unwrap <code>&lt;noscript&gt;</code> and run Readability again. Many modern pages are JavaScript shells with the real content sitting inside <code>&lt;noscript&gt;</code> for search engines.</li>
<li>Look for <code>articleBody</code> in JSON-LD. It is not common, but checking it is almost free.</li>
<li>Try <code>&lt;article&gt;</code>, <code>&lt;main&gt;</code>, and <code>[role=main]</code>.</li>
<li>As a last resort, take the entire <code>&lt;body&gt;</code> and aggressively remove navigation, headers, footers, and obvious noise.</li>
</ol>
<p>One small detail cost me a real bug: <code>&lt;noscript&gt;</code> must only be unwrapped after the first attempt fails. If I unwrap it immediately, perfectly good pages sometimes acquire an "Enable JavaScript to continue" message right in the middle of the article.</p>
<p>The order matters. It often does.</p>
<h2 id="the-2-2-4-that-made-me-rewrite-everything">The <code>&gt;&gt;&gt; 2 + 2 4</code> that made me rewrite everything</h2>
<p>My first HTML-to-text converter was handwritten. A recursive <code>walk()</code>, around sixty lines, and it worked.</p>
<p>Then I converted the Python tutorial.</p>
<pre><code class="language-text">&gt;&gt;&gt; 2 + 2 4 &gt;&gt;&gt; 50 - 5*6 20 &gt;&gt;&gt; 8 / 5 # division always
returns a floating-point number 1.6
</code></pre>
<p>That was supposed to be a code block. Every <code>&gt;&gt;&gt;</code> should begin a new line, and every result should appear on the following line. My converter flattened the whole thing into one paragraph. The code became alphabet soup.</p>
<p>The problem was not a small bug. The problem was that I had underestimated the job.</p>
<p>Converting HTML to readable text looks easy until you try to do it properly. A <code>&lt;pre&gt;</code> block must preserve every meaningful space. Tables need aligned columns. Nested ordered lists can start at five and use Roman numerals. A quotation needs <code>&gt; </code> on every wrapped line, not only the first one.</p>
<p>Each edge case is an afternoon of work followed by another afternoon of discovering what the first afternoon broke. Multiply that by ten and I would spend a week badly rebuilding what <code>html-to-text</code> has already handled well for years.</p>
<p>So I replaced my converter. The same Python example now comes out like this:</p>
<pre><code class="language-text">&gt;&gt;&gt; 2 + 2
4
&gt;&gt;&gt; 50 - 5*6
20
</code></pre>
<p>The RFC 9110 method table comes out aligned as well:</p>
<pre><code class="language-text">Method   Safe  Idempotent  Section
CONNECT  no    no          9.3.6
GET      yes   yes         9.3.1
POST     no    no          9.3.3
</code></pre>
<p>Before that, a table could become <code>NameQtyApple10</code>.</p>
<p>I am not exaggerating.</p>
<p>I kept a few formatting decisions of my own on top of the library. Headings use <code>=</code> and <code>-</code> underlines, the way plain-text README files have done for decades, and links become numbered references.</p>
<h2 id="links-should-not-ruin-the-sentence">Links should not ruin the sentence</h2>
<p>This part is a matter of taste, and I will defend mine.</p>
<p>A link contains two pieces of information: the words you read and the address they point to. Put the complete address inline and the paragraph becomes painful to read. Delete the address and you destroy information.</p>
<p>So text2.site turns a link into <code>the words [4]</code> and places the address in a reference list at the bottom. Academic writing solved this problem a century ago. There was no reason for me to invent a worse answer.</p>
<p>I added two rules that made a surprising difference.</p>
<p>First, the same URL always gets the same number. If an article cites one source five times, the reader sees <code>[1]</code> five times, not five separate references pretending to be different things.</p>
<p>Second, fragment links do not become references. A <code>#section-3</code> link from a table of contents is useless once the page becomes a text file. Removing those links reduced the RFC 9110 output from <strong>3,017 references to 1,166</strong>. Nearly two thousand lines of noise disappeared because of one rule.</p>
<h2 id="the-coffee-that-became-caf">The coffee that became <code>caf?</code></h2>
<p>This bug was embarrassing, which is probably why it became one of my favorites.</p>
<p>The service read the response bytes and called <code>.toString('utf8')</code>. Always. No questions asked.</p>
<p>Unfortunately, a meaningful part of the web still arrives as <code>windows-1252</code> or <code>ISO-8859-1</code>: old institutional websites, city government pages, a CMS from 2011, a personal blog that never migrated. Words like <code>café</code> and <code>ação</code> arrived mangled.</p>
<p>The fix follows the order defined by HTML. Check the BOM first because it is unambiguous. Then inspect the HTTP header. Then look for <code>&lt;meta charset&gt;</code> inside the first 1,024 bytes. Use UTF-8 only as the final fallback.</p>
<p>There is one more practical trick. If a page insists that it is UTF-8 but the decoded result is full of replacement characters, I decode it again as <code>windows-1252</code> and keep whichever version is less broken. Standards are useful. So is noticing when a website is lying.</p>
<p>The best part is that this required zero new dependencies. Node's own <code>TextDecoder</code> already supports <code>shift_jis</code>, <code>gb18030</code>, <code>big5</code>, <code>koi8-r</code>, and the rest. I only needed to confirm that the Alpine Node image ships with full ICU support. A thirty-second <code>docker exec</code> turned a vague doubt into a fact.</p>
<h2 id="the-day-i-realized-i-had-built-an-open-proxy">The day I realized I had built an open proxy</h2>
<p>Everything looked harmless while the service ran on <code>localhost</code>. Then I prepared to put it on a public domain and the obvious finally became visible.</p>
<p>The <code>/extract</code> endpoint fetches any URL it receives. On a VPS, that means a stranger could ask <strong>my server</strong> to request <code>http://127.0.0.1:5432</code>, an internal network address, or the cloud metadata endpoint at <code>169.254.169.254</code>, which can expose credentials on some providers.</p>
<p>It was not exactly a bug in one line of code. It was a consequence of deployment waiting patiently to bite me.</p>
<p>Now every hostname is resolved before the request. The service rejects loopback, private, link-local, CGNAT, and multicast ranges in both IPv4 and IPv6, including mapped forms such as <code>::ffff:10.0.0.1</code>.</p>
<p>The detail that nearly escaped me was automatic redirection. With <code>redirect: 'follow'</code>, the first URL can pass validation and the second hop can land directly on the metadata endpoint. Redirects are now followed manually, one at a time, with every destination checked again. The maximum is five.</p>
<p>One hole remains, and I document it because pretending otherwise would not close it. I resolve and validate the address, then <code>fetch</code> performs DNS resolution again. A record that changes between those two moments can slip through. That is DNS rebinding. Closing it correctly means pinning the connection to the IP address that was already verified.</p>
<p>It is on the list.</p>
<h2 id="lightweight-is-not-an-aesthetic-it-is-a-measurement">Lightweight is not an aesthetic. It is a measurement.</h2>
<p>The project originally used <code>jsdom</code>. It is the standard DOM implementation for Node, and it works.</p>
<p>Then I measured it.</p>
<table>
<thead>
<tr>
<th>Measurement</th>
<th style="text-align:right">jsdom</th>
<th style="text-align:right">linkedom</th>
</tr>
</thead>
<tbody>
<tr>
<td>Installed size</td>
<td style="text-align:right">25 MB</td>
<td style="text-align:right"><strong>5.6 MB</strong></td>
</tr>
<tr>
<td>Load time</td>
<td style="text-align:right">497 ms</td>
<td style="text-align:right"><strong>108 ms</strong></td>
</tr>
<tr>
<td>Memory while processing Wikipedia</td>
<td style="text-align:right">+17 MB</td>
<td style="text-align:right"><strong>+1 MB</strong></td>
</tr>
</tbody>
</table>
<p>I switched to <code>linkedom</code>.</p>
<p>The migration needed two fixes, both discovered by comparing the output side by side. <code>linkedom</code> exposes the contents of <code>&lt;template&gt;</code> as normal children, allowing boilerplate to leak into the text. It also has no base URL option, so relative links require a manually injected <code>&lt;base href&gt;</code>.</p>
<p>The final result was worth it. <code>node_modules</code> went from 25 MB to <strong>6.3 MB</strong>, and somewhere along the way the project also gained a proper text renderer. Less weight and more capability at the same time.</p>
<p>That almost never happens.</p>
<h2 id="a-container-and-a-quota-of-ten">A container and a quota of ten</h2>
<p>text2.site runs in Docker. The image uses two stages, the process runs as an unprivileged user, the filesystem is read-only, all Linux capabilities are dropped with <code>cap_drop: ALL</code>, and <code>dumb-init</code> runs as PID 1 so the <code>SIGTERM</code> from <code>docker stop</code> actually reaches Node instead of waiting ten seconds for a kill.</p>
<p>The port binds to <code>127.0.0.1</code>, nothing else. The container can only be reached through nginx. As far as the public internet is concerned, the container itself does not exist.</p>
<p>There is also a quota: <strong>ten successful conversions per IP every 24 hours</strong>. This is not the beginning of a premium plan. It is there so the server can remain cheap enough to stay open to everyone.</p>
<p>One rule matters to me: <strong>failure does not spend quota</strong>. Invalid URL, unavailable website, page with no extractable article - the attempt is returned. Only an actual result counts. Charging the user for my failure would be ugly.</p>
<p>The counters live in memory, and a restart clears them. That is a deliberate tradeoff, not an oversight. Deploying a database to preserve ten integers would cost more than the abuse it might prevent.</p>
<h2 id="what-i-deliberately-did-not-build">What I deliberately did not build</h2>
<p>Pages rendered entirely in JavaScript do not work. Paywalls do not work. Content behind a login does not work.</p>
<p>I could support them with a headless browser. I will not.</p>
<p>A Chromium process can consume 300 MB of RAM per page and take seconds for each conversion. At that point I would have sacrificed exactly what I was trying to preserve: a small service that does one thing without dragging an operating system behind it.</p>
<p>When extraction fails, the website says so. A clear error is better than returning a navigation bar disguised as an article.</p>
<h2 id="what-building-it-taught-me">What building it taught me</h2>
<p>Three things stayed with me.</p>
<p><strong>First:</strong> most of the quality came from work performed before and after the main library. Readability is excellent, but it can only understand the DOM I give it. Removing hidden elements, excluding <code>&lt;template&gt;</code>, and stripping the little <code>¶</code> symbols that Sphinx and MDN attach to headings are preparation, not magic. That preparation changed the result.</p>
<p><strong>Second:</strong> measurement is cheap and opinion is expensive. "jsdom is heavy" was only a feeling until I had 25 MB and 5.6 MB on the screen. Half an hour of benchmarking settled a question I could have argued about for a week.</p>
<p><strong>Third:</strong> writing tests after the system already works feels like wasted time. It is not. The 42 tests I wrote at the end immediately found two problems: I was removing <code>&lt;script&gt;</code> elements before looking for JSON-LD inside them, and a short but legitimate page such as <code>example.com</code> had started being rejected. Both bugs would have reached production quietly.</p>
<p>text2.site has one screen, one text box, and one button.</p>
<p>It took much more work than it appears to contain.</p>
<p>That is the point.</p>
<p><strong><a href="https://text2.site">text2.site</a></strong> - paste the URL, get the text.</p>
]]></content:encoded>
    </item>
    <item>
        <title>Coffin Joe</title>
        <link>https://pablomurad.com/coffin-joe/</link>
        <guid isPermaLink="true">https://pablomurad.com/coffin-joe/</guid>
        <pubDate>Mon, 20 Jul 2026 18:16:24 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>people</category>
        <description>José Mojica Marins birthed Brazil&#39;s greatest horror icon, Zé do Caixão (Coffin Joe). Using cheap sets and pure defiance, he fought poverty and censorship to forge a rebellious mythology that forever changed Brazilian cinema.</description>
        <content:encoded><![CDATA[<p>Brazil did not need a castle in Transylvania. It had a cemetery, a black top hat, a pair of impossible fingernails, and José Mojica Marins staring into the camera as if he had personally caught the audience trespassing.</p>
<p>That was enough.</p>
<p>Zé do Caixão — known abroad as Coffin Joe — is one of those characters who seem too complete to have been invented. He looks like folklore that has always existed: undertaker, blasphemer, philosopher, sadist, carnival barker, village tyrant. But he came from one man, one nightmare, and a kind of Brazilian cinema that had almost no money and absolutely no interest in asking permission.</p>
<p>What fascinates me about José Mojica Marins is not only that he created Brazil's greatest horror icon. It is that he built an entire mythology with whatever was available. Cheap sets. Amateur performers. Real animals. Television studios. Censored footage. His own face. His own nails. If an elegant production system did not exist for the films he wanted to make, he simply crawled under the system and kept filming from there.</p>
<h2 id="a-boy-who-grew-up-inside-a-cinema">A Boy Who Grew Up Inside a Cinema</h2>
<p>Mojica was born in São Paulo on March 13, 1936. A Friday the 13th, naturally. If a publicist had invented that detail later, it would feel embarrassingly obvious. Reality was less subtle.</p>
<p>His father managed a neighborhood movie theater, and the family lived in the same building. Mojica spent his childhood close to the projection room, surrounded by reels, shadows, posters, and the mechanical rhythm of films passing through a projector. Cinema was not an occasional event for him. It was the architecture of the house.</p>
<p>At twelve, he received an 8mm camera and began making films with relatives, neighbors, and anyone willing to stand in front of the lens. He exhibited those early productions wherever he could and charged small admissions to recover the costs. By his late teens, he had founded an acting school and a small film company. The school supplied performers, trained technicians, and helped finance the next production. It was less an institution than a self-sustaining cinematic organism.</p>
<p>Before horror, he tried westerns, adventures, melodramas, and other popular genres. His first completed feature, <em>A Sina do Aventureiro</em> (<em>The Adventurer's Fate</em>, 1958), was a Brazilian western assembled with the same stubborn independence that would define the rest of his career. Mojica was not waiting to be invited into an established industry. He was manufacturing an industry around himself.</p>
<p>This matters because Zé do Caixão did not emerge from a comfortable studio searching for a marketable monster. He emerged from necessity. Mojica needed a character strong enough to survive poverty, censorship, bad equipment, skeptical actors, and audiences who had never seen a Brazilian horror film before.</p>
<p>So he made someone impossible to ignore.</p>
<h2 id="the-nightmare-that-became-z%C3%A9-do-caix%C3%A3o">The Nightmare That Became Zé do Caixão</h2>
<p>Mojica said the character arrived in a nightmare in 1963. A dark figure dragged him from his bed and carried him toward a grave. Inside the grave, Mojica saw his own body waiting in a coffin. He woke up terrified and began developing the story that became <em>À Meia-Noite Levarei Sua Alma</em> — <em>At Midnight I'll Take Your Soul</em>.</p>
<p>The dream explains the visual shell, but the character became much stranger than a simple supernatural undertaker. Zé is not Dracula, a ghost, or a demon. He is a human being who has decided that conventional morality is a weakness. He despises religion, mocks superstition, terrorizes his neighbors, and believes that the only form of immortality is “the continuation of blood”: producing a perfect son with the perfect woman.</p>
<p>Naturally, everyone around him becomes disposable.</p>
<p><img src="https://s3.amazonaws.com/nightjarprod/content/uploads/sites/197/2024/08/20141739/sbZPx7322ZUvBJa7qTxhNmVBqTY.jpg" alt="Poster for At Midnight I'll Take Your Soul" loading="lazy"></p>
<p><em>Poster for <a href="https://sunscinema.com/movies/at-midnight-ill-take-your-soul/">At Midnight I'll Take Your Soul</a>. Artwork © its respective rights holders, remotely displayed for identification and editorial commentary.</em></p>
<p>Released in 1964, <em>At Midnight I'll Take Your Soul</em> is widely regarded as the first Brazilian horror feature. It is raw, theatrical, angry, and much more inventive than its limited resources should allow. The film does not politely introduce Zé. He addresses the audience, challenges God, eats meat on Good Friday while a religious procession passes, assaults people who irritate him, and murders anyone obstructing his search for an heir.</p>
<p>There is something almost punk about the film, although it predates punk by more than a decade. It attacks social and religious authority, but it does not replace them with a clean political program. Zé's rebellion is selfish, violent, and grotesque. He is not a misunderstood hero. He is the nightmare produced when absolute individualism loses the last trace of compassion.</p>
<p>And yet he is magnetic.</p>
<p>That is Mojica's trick. Zé is horrible, but never dull. He enters a room and changes its temperature. The hat makes him taller. The cape gives him the silhouette of a nineteenth-century villain who somehow wandered into a poor Brazilian town. The beard, rings, and fingernails push him toward caricature, but Mojica's eyes pull him back into danger. He does not merely look at other characters. He seems to accuse them of being alive incorrectly.</p>
<p><img src="https://core-cms.bfi.org.uk/sites/default/files/styles/responsive/public/2026-04/at-midnight-ill-take-your-soul-1964-coffin-joe-hand-across-chest.jpg/1024x521-cropped/at-midnight-ill-take-your-soul-1964-coffin-joe-hand-across-chest.jpg" alt="José Mojica Marins as Zé do Caixão in At Midnight I'll Take Your Soul" loading="lazy"></p>
<p><em>José Mojica Marins as Zé do Caixão in <a href="https://www.bfi.org.uk/lists/10-great-brazilian-horror-films">At Midnight I'll Take Your Soul</a>. Film still hosted by the British Film Institute; image © its respective rights holders.</em></p>
<h2 id="the-most-brazilian-thing-about-him">The Most Brazilian Thing About Him</h2>
<p>Calling the character “Brazilian Dracula” is convenient, but it misses what makes him special. Zé has no aristocratic castle and no ancient European bloodline. He is a local undertaker. He lives among ordinary people, argues with villagers, humiliates the faithful, and uses the daily intimacy of a small community as a weapon.</p>
<p>The horror is not visiting from somewhere else. It already owns a business in town.</p>
<p>Mojica borrowed the visual confidence of classic horror, but he filled it with Brazilian tensions: Catholic ritual, popular superstition, poverty, social hierarchy, masculine arrogance, and the constant friction between official respectability and the violence underneath it. The films feel handmade because they were handmade, but that roughness is not merely a technical deficiency. It gives them texture. Their world seems dirty enough to leave residue on the viewer.</p>
<p>The first film was a commercial success despite financial trouble and even ridicule from people around the production. Zé quickly escaped the screen. He appeared in comic books, records, television programs, public events, and eventually advertising. Mojica and the character became so closely fused that many Brazilians simply called the director Zé do Caixão.</p>
<p>It is one of the strangest victories in cinema: the monster ate the name of his creator, and the creator seemed delighted to keep feeding him.</p>
<h2 id="hell-suddenly-has-color">Hell Suddenly Has Color</h2>
<p>Mojica continued the story with <em>Esta Noite Encarnarei no Teu Cadáver</em> — <em>This Night I'll Possess Your Corpse</em> — released in 1967. Zé survives the punishment at the end of the first film and resumes his search for the woman capable of bearing his “superior” child. His methods become more elaborate and more cruel. Women are abducted and subjected to tests involving snakes and spiders. The philosophical speeches grow larger. So does the madness.</p>
<p><img src="https://s3.amazonaws.com/nightjarprod/content/uploads/sites/197/2024/08/26144119/oI1sj3noMxG21IPByWOPJcYFDhg1-683x1024.jpg" alt="Poster for This Night I'll Possess Your Corpse" loading="lazy"></p>
<p><em>Poster for <a href="https://sunscinema.com/movies/this-night-ill-possess-your-corpse-2/">This Night I'll Possess Your Corpse</a>. Artwork © its respective rights holders, remotely displayed for identification and editorial commentary.</em></p>
<p>The film is mostly black and white, but its famous vision of Hell erupts into color. It is a beautiful decision because the color does not make the sequence more realistic. It makes it less stable. Bodies twist among crude, painted landscapes and impossible shapes. The limited production values stop being a weakness and become the logic of a fever dream.</p>
<p>Mojica often used real animals. The spiders, snakes, rats, and cockroaches were not polite effects added later. In a 2009 interview, he explained that he would submit himself to the animals during rehearsals before asking actors to do the same. This does not make the working conditions easy to defend by modern standards, but it complicates the popular image of Mojica standing safely behind the camera while terrorizing everyone else. He wanted the ordeal to be physical, and he put his own body into it.</p>
<p>The censorship was just as real. Mojica worked through the Brazilian military dictatorship, when blasphemy, sexuality, drugs, and attacks on moral authority attracted official attention. The ending of <em>This Night I'll Possess Your Corpse</em> was forced toward a religious concession that contradicted Zé's philosophy. Decades later, Mojica described <em>Embodiment of Evil</em> as a chance to let the character finally stand against what the censors had demanded.</p>
<p>The state could cut the film. It could not make the character obedient.</p>
<h2 id="the-film-the-dictatorship-tried-to-bury">The Film the Dictatorship Tried to Bury</h2>
<p>Zé do Caixão was Mojica's most famous creation, but Mojica was more than the caretaker of a single franchise. He made anthology horror, westerns, social satires, erotic films, hallucinations, and works that barely fit into any stable category.</p>
<p>One of the most fascinating is <em>O Ritual dos Sádicos</em>, later renamed <em>O Despertar da Besta</em> — <em>Awakening of the Beast</em>. Completed around 1970, it mixes documentary-style discussions of drugs and social decay with dramatized violence and an LSD experiment built around the image of Zé do Caixão. The federal censors rejected it. The film remained prohibited for roughly fifteen years and was only released in the 1980s under its new title.</p>
<p><img src="https://ims.com.br/wp-content/uploads/2024/11/O-despertar-da-besta_1920x1080.jpg" alt="Scene from Awakening of the Beast" loading="lazy"></p>
<p><em>Scene from <a href="https://ims.com.br/mostra/jose-mojica-marins-restaurado/">Awakening of the Beast</a>, presented by Instituto Moreira Salles as part of its José Mojica Marins restoration program. Image © its respective rights holders.</em></p>
<p>There is a dark joke hiding in that history. A government obsessed with protecting society from dangerous images turned a low-budget filmmaker into forbidden mythology. Mojica already understood how publicity worked. Censorship merely gave him another costume.</p>
<p>He also brought horror into Brazilian homes. Beginning in the late 1960s, he appeared as Zé do Caixão on television, introducing and performing horror stories. Many recordings were later erased or reused, leaving a painful gap in Brazilian television history. In the 1990s, a new generation encountered him through <em>Cine Trash</em>, where he presented horror movies on TV Bandeirantes. He was no longer only a film character. He was a host, a mascot, and a familiar intruder in the afternoon schedule.</p>
<p>Outside Brazil, the rediscovery took longer. In the mid-1990s, American cult-video audiences began finding Mojica through VHS releases. The English name Coffin Joe helped package the character, but the films remained stubbornly themselves: Portuguese-speaking, Catholic-haunted, cheap, theatrical, and unmistakably Brazilian. What foreign viewers treated as a bizarre new discovery had already been living in Brazil's cultural basement for three decades.</p>
<h2 id="forty-four-years-to-finish-a-trilogy">Forty-Four Years to Finish a Trilogy</h2>
<p>The third official chapter, <em>Encarnação do Demônio</em> — <em>Embodiment of Evil</em> — arrived in 2008, forty-four years after the first film. That gap alone makes the trilogy extraordinary. The same filmmaker returned to the same character not as a nostalgic cameo, but as an old man still determined to complete an argument interrupted by censorship, money, lost rights, and time.</p>
<p><img src="https://www.datocms-assets.com/137966/1730219113-banner-1080x1920px.png" alt="Poster for Embodiment of Evil" loading="lazy"></p>
<p><em>Official promotional artwork for <a href="https://www.gullane.com.br/projetos/encarnacao-do-demonio/">Embodiment of Evil</a>, remotely hosted by Gullane. Artwork © its respective rights holders.</em></p>
<p>In the film, Zé is released after forty years in a prison psychiatric ward and returns to a modern São Paulo that has changed without becoming less violent. He resumes his search for the perfect woman and the continuation of his blood. The production is larger, the effects are more explicit, and the filmmaking is technically polished, but Mojica does not domesticate the character for a respectable comeback.</p>
<p>The movie also contains a moving production story. Veteran actor Jece Valadão became seriously ill during filming and died before completing his role. Rather than replace him and erase his final work, Mojica insisted that Valadão remain in the movie. The screenplay was restructured around the footage already filmed. For a director famous for cruelty on screen, it was a deeply loyal decision behind the camera.</p>
<p>And then there were the tarantulas. This time Mojica let them crawl across his own face on camera, partly as an answer to decades of accusations that he made performers endure things he would not face himself. At more than seventy years old, he was still proving the point physically. Sensible? Perhaps not. Consistent? Completely.</p>
<h2 id="a-filmmaker-too-large-for-%E2%80%9Cso-bad-its-good%E2%80%9D">A Filmmaker Too Large for “So Bad It's Good”</h2>
<p>Mojica spent years being dismissed as trash, exploitation, incompetence, or accidental comedy. Some of the films are uneven. Some effects are visibly improvised. Performances can swing from stiff to hysterical inside the same scene. But “so bad it's good” is a lazy way to describe an artist who knew exactly how to create an image people could not forget.</p>
<p>He understood faces, silhouettes, ritual, repetition, and confrontation. He understood that the camera could be addressed directly, almost assaulted. He understood that a limited set becomes convincing when the actor inside it behaves as if the universe ends at the wall. Most importantly, he understood that horror does not require permission from realism.</p>
<p>The films have humor, but they are not jokes. Their roughness belongs to the conditions that produced them and to Mojica's refusal to wait for better conditions. He was making personal genre cinema in Brazil before the cultural gatekeepers had a comfortable category for it. While more prestigious movements received academic respect, Mojica reached popular audiences and built an icon from the margins.</p>
<p>Today his work is being restored in 4K from original materials, screened by institutions such as the Instituto Moreira Salles and the BFI, and reconsidered as a central part of Brazilian and world horror cinema. The restoration is important because poor VHS copies shaped the international reputation of these films for years. Mojica's darkness was intentional. The mud was not always supposed to be there.</p>
<p>José Mojica Marins died in São Paulo in 2020, at the age of 83, from complications of bronchopneumonia. Zé do Caixão survived him, of course. Characters built around immortality tend to be annoying that way.</p>
<p>But I think the real continuation of blood was never the perfect son Zé kept demanding. It was the filmmakers Mojica influenced, the audiences he disturbed, the images he planted in Brazilian culture, and the proof that horror made here did not need to imitate anyone politely.</p>
<p>Brazil did not inherit Zé do Caixão from an old legend.</p>
<p>Mojica made the legend himself.</p>
]]></content:encoded>
    </item>
    <item>
        <title>Susan Kare Made the Computer Feel Human</title>
        <link>https://pablomurad.com/susan-kare-made-the-computer-feel-human/</link>
        <guid isPermaLink="true">https://pablomurad.com/susan-kare-made-the-computer-feel-human/</guid>
        <pubDate>Mon, 20 Jul 2026 17:59:10 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>stories</category>
        <description>Susan Kare shaped personal computing by designing the original Macintosh icons. Her intuitive, friendly pixel art—like the smiling Mac and the Command symbol—gave machines a welcoming face and made them accessible to everyday users.</description>
        <content:encoded><![CDATA[<p>There are people whose work becomes so familiar that it almost disappears. We use it, understand it, and move on without ever asking who made it. Susan Kare belongs to that rare group. Even if someone does not know her name, there is a good chance they have already met her ideas: the smiling Macintosh, the trash can, the paint bucket, the lasso, the Chicago typeface, the Command symbol, and even the cards in Microsoft Solitaire.</p>
<p>What I find remarkable is not simply that Kare designed a collection of famous icons. She helped teach ordinary people how to speak with a computer at a moment when most computers still felt like machines built for specialists. She gave the Macintosh a face, but more importantly, she gave it manners.</p>
<p><img src="https://commons.wikimedia.org/wiki/Special:Redirect/file/Apple_Mac_128.jpg?width=1280" alt="An Apple Macintosh 128K with its keyboard and mouse" loading="lazy"></p>
<p><em>The Macintosh 128K. Photograph by Christo, licensed under <a href="https://creativecommons.org/licenses/by-sa/4.0/">CC BY-SA 4.0</a>, via <a href="https://commons.wikimedia.org/wiki/File:Apple_Mac_128.jpg">Wikimedia Commons</a>.</em></p>
<h2 id="before-the-mac-there-was-graph-paper">Before the Mac, There Was Graph Paper</h2>
<p>Kare did not arrive at Apple as a computer expert. She had studied art, completed a doctorate in fine arts, worked in museums, and made sculpture. In a 2000 interview preserved by Stanford, she said her experience with computer graphics before Apple was exactly zero. That fact is usually presented as a charming piece of trivia, but I think it explains a great deal about why her work succeeded.</p>
<p>The Macintosh was supposed to reach people who did not already understand computers. Kare was one of those people. She was not trapped inside the assumptions of an industry that expected users to memorize commands and adapt themselves to the machine. She could look at an unfinished interface and feel the same uncertainty that a future customer might feel.</p>
<p>Her way into Apple came through Andy Hertzfeld, a high school friend who was working on the Macintosh team. He told her that the machine needed small graphics and suggested that she draw them on graph paper, one square at a time. She initially did the work in exchange for an Apple II. Later, she joined the project and learned the technical side while working.</p>
<p>The method was wonderfully direct. Each square on the paper represented a pixel. Fill one in, leave another empty, and gradually an object appeared. Kare compared bitmap graphics to mosaics and needlepoint, forms she already understood. The technology was new, but the underlying problem was ancient: how do you build a clear image from tiny individual pieces?</p>
<p>The limitations were severe. A typical icon had to survive inside a tiny black-and-white grid. There was no smooth shading to hide a weak silhouette, no huge canvas, and no high-resolution display to rescue an unnecessary detail. Every pixel had to earn its place. That constraint could have produced a cold and purely functional system. Instead, Kare found room for expression.</p>
<h2 id="a-computer-that-smiled-back">A Computer That Smiled Back</h2>
<p>The original Macintosh did something emotionally intelligent before the user opened a document or clicked a menu: it greeted them with a smiling computer. The Happy Mac was extremely simple, yet it immediately changed the mood of the encounter. The machine was not presenting a wall of technical information. It was telling you, in the smallest possible visual language, that everything was fine.</p>
<p>That friendliness continued throughout the interface. Files looked like pieces of paper. Tools looked like tools. A trash can offered a visible place to discard something. MacPaint used a brush, a hand, a paint bucket, and a lasso. These images did not merely decorate the software. They made actions easier to predict.</p>
<p>The Museum of Modern Art describes Kare's icons as a language intended to be understandable across countries. That ambition matters. A successful icon is not just attractive. It should communicate quickly, stay recognizable at a terrible resolution, and remain memorable after the user learns it once. Kare herself said that a good icon should either be instantly understandable or easy to remember after a single explanation.</p>
<p>Her process was practical rather than mystical. She created alternatives, showed them to people, listened to their reactions, and refined the result. When the MacPaint team needed an icon for filling an area, she explored paint rollers and other possibilities. The pouring paint can won because people understood it. That is such a healthy design lesson: the cleverest idea is not automatically the clearest one.</p>
<p>Some experiments did fail. At one point, Kare tried to represent copying with a cat looking into a mirror — a literal “copycat.” It was funny, but the pun would not travel across languages. She abandoned it. The story makes me like her work even more because it shows the judgment behind the simplicity. Good visual design is not the absence of playful ideas. It is knowing which playful idea belongs in the final interface.</p>
<h2 id="the-command-symbol-has-a-strange-little-history">The Command Symbol Has a Strange Little History</h2>
<p>One of Kare's most enduring contributions began with Steve Jobs complaining that the Apple logo appeared too many times in the menus. Keyboard shortcuts were marked with a small Apple, and Jobs wanted another symbol immediately.</p>
<p>Kare searched through a book of international symbols and found the looped-square sign now associated with the Command key. The exact folklore around it became muddled over time. An early account described it as a Swedish campground symbol for an interesting feature. Swedish readers later corrected the detail: the sign is associated more broadly with places of interest or historical sites. The important part is that Kare did not pretend a random shape already had meaning. She searched for a real symbol, selected one that remained legible as a small bitmap, and gave it a new technological life.</p>
<p><img src="https://commons.wikimedia.org/wiki/Special:Redirect/file/Key--Command_%281990s%29.svg?width=960" alt="A Macintosh Command key showing the Apple and looped-square symbols" loading="lazy"></p>
<p><em>A Macintosh Command key from the 1990s. Illustration by Daniel Beardsmore, released into the public domain, via <a href="https://commons.wikimedia.org/wiki/File:Key--Command_%281990s%29.svg">Wikimedia Commons</a>.</em></p>
<p>Today I can see ⌘ in a menu and understand it without reading a word. It feels inevitable, as if computers always had to use that shape. They did not. Someone had to notice it in a reference book, understand its potential, redraw it for a tiny grid, and persuade everyone else that it worked. Invisible decisions like this are what make Kare's career so interesting.</p>
<h2 id="she-also-gave-the-macintosh-its-voice">She Also Gave the Macintosh Its Voice</h2>
<p>Kare's work was not limited to pictures. She designed bitmap typefaces for the Macintosh, including Chicago, Geneva, Monaco, New York, and the wonderfully strange pictographic font Cairo. Each font had to remain readable on a low-resolution screen, which meant making decisions letter by letter and pixel by pixel.</p>
<p>Chicago became the loud, compact voice of the classic Mac interface. It appeared in menus and system text for years, and later survived on early iPods. Monaco offered monospaced clarity. Cairo replaced conventional letters with small images and symbols, including the creature that became Clarus the Dogcow, an Apple cult character that began as a tiny glyph and somehow acquired a name, a sound, and a life of its own.</p>
<p>People sometimes describe Cairo as a precursor to emoji. That comparison makes sense as long as we do not flatten the history. Cairo was not the direct origin of modern emoji, but it demonstrated how pictograms could live inside a font and enter text as characters. It belonged to the same larger human desire: sometimes a small picture communicates a tone or idea more quickly than another sentence.</p>
<p>Kare later brought the same precision to other platforms. For Microsoft, she designed interface graphics for Windows 3.0 and the card deck used in Solitaire. Those cards appeared on an absurd number of computers, teaching generations of users to click, drag, and release with a mouse while they thought they were only wasting a few minutes. Her work also reached NeXT, IBM, General Magic, Facebook, Pinterest, and many other companies.</p>
<p>This is another reason I admire her. Her legacy cannot be reduced to nostalgia for beige Macs. She developed a way of thinking about interface graphics — direct, economical, friendly, and grounded in recognition — then carried it across very different products.</p>
<h2 id="the-art-was-small-but-it-was-never-minor">The Art Was Small, but It Was Never Minor</h2>
<p>For a long time, software icons were treated as details. Programmers built the “real” system, while the pictures were assumed to be finishing touches. Kare's work exposes how wrong that distinction is. The icons were part of the machine's behavior. They shaped what people noticed, what they understood, and whether they felt comfortable experimenting.</p>
<p>There is a wonderful contradiction in her career. The images were tiny, but their cultural reach was enormous. They were black and white, yet full of personality. They were designed to disappear into use, but they eventually entered museum collections. In 2015, MoMA acquired her 1982 Macintosh icon sketchbook, preserving the graph-paper drawings as part of design history. Kare received the AIGA Medal in 2018 and Cooper Hewitt's Lifetime Achievement award in 2019.</p>
<p>That recognition was deserved, but also revealingly late. We are still learning to credit the people who shaped the everyday language of software. An interface becomes “intuitive” only after designers have spent a ridiculous amount of time deciding what should be visible, what can be removed, and how a person will interpret a handful of marks on a screen.</p>
<p>Kare once explained that sparse, refined design can make a system feel easier to use. I keep returning to that idea because it sounds obvious only after someone says it. Clutter does more than make a screen ugly; it makes the machine feel less certain. A well-made icon reduces that uncertainty. It does not show off. It helps.</p>
<h2 id="why-her-work-still-matters">Why Her Work Still Matters</h2>
<p>Modern interfaces have more pixels, more colors, more animation, and far more processing power than the first Macintosh. Yet they are not automatically clearer. In fact, abundance often creates its own laziness. When every icon can have gradients, shadows, movement, and microscopic detail, it becomes tempting to solve a communication problem with decoration.</p>
<p>Kare's work is a useful correction. Start with meaning. Make the silhouette readable. Remove what is not helping. Test whether another person understands it. Give the object enough character to be memorable, but never so much personality that function disappears.</p>
<p>What I love most is the humanity inside that discipline. Kare did not make the computer friendlier by covering it in empty cheerfulness. She made it friendlier by respecting the person sitting in front of it. The smile mattered because the rest of the interface kept the same promise: you should be able to look, try, make a mistake, and continue.</p>
<p>Susan Kare did not merely give the Macintosh its face. She helped establish the visual grammar that made personal computers feel personal. Decades later, her best work still performs the same trick. We recognize it immediately, use it without ceremony, and almost forget that someone had to draw every single pixel.</p>
]]></content:encoded>
    </item>
    <item>
        <title>Movie: Bloodlust</title>
        <link>https://pablomurad.com/movie-bloodlust/</link>
        <guid isPermaLink="true">https://pablomurad.com/movie-bloodlust/</guid>
        <pubDate>Sun, 19 Jul 2026 21:37:14 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>movies</category>
        <description>A low-budget production, a real Romanian landscape and one of the most memorable vampires of the 1990s.</description>
        <content:encoded><![CDATA[
<!--kg-card-begin: html-->
<iframe title="Subspecies" width="560" height="315" src="https://pablo.tube/videos/embed/qhkMc8XcLvrUKrrnydvD7c" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
<!--kg-card-end: html-->
<p>Some vampire films are polished until there is nothing dangerous left in them. The castles are perfect, the costumes are expensive, the blood is tastefully arranged and the vampire looks ready for a perfume advertisement.</p>
<p><em>Subspecies</em> is not that kind of film.</p>
<p>Released in 1991 by Full Moon Entertainment and directed by Ted Nicolaou, <em>Subspecies</em> feels old, strange and slightly unclean in the best possible way. It has ruins, forests, local legends, small creatures crawling across stone floors and a vampire who looks as if he has spent centuries sleeping somewhere that nobody should open.</p>
<p>I liked it a lot.</p>
<p>It is not a flawless movie, and I do not need it to be one. What it has is atmosphere, personality and a villain who immediately belongs to the screen. In a genre filled with elegant counts and romantic immortals, Radu arrives with long fingers, a ruined face, a wet voice and absolutely no interest in being charming.</p>
<p>Well, perhaps he thinks he is charming.</p>
<p>That only makes him better.</p>
<h2 id="the-story-of-the-first-subspecies">The story of the first Subspecies</h2>
<p><em>This section discusses the complete story, including the ending.</em></p>
<p>The film opens inside Castle Vladislas, where King Vladislav is confronted by his exiled son Radu. The king, played by Angus Scrimm, has maintained an old agreement between vampires and the nearby human population. A mystical relic called the Bloodstone provides blood said to come from the saints, allowing the vampires to survive without feeding on the villagers.</p>
<p>Radu does not care much for peaceful coexistence.</p>
<p>He murders his father and takes the Bloodstone, not only because he desires its power, but because he sees it as his inheritance. This first scene establishes everything important about him. Radu is not simply hungry. He is resentful. He believes the world has denied him something, and therefore everything he does afterward becomes, in his mind, a form of correction.</p>
<p>At roughly the same time, three students arrive in the Romanian town of Prejmer. Michelle and Lillian are Americans visiting their Romanian friend Mara. They are there to study local culture, myths and superstitions, which is the sort of academic plan that sounds wonderful until the local superstition begins walking behind you at night.</p>
<p>The women stay in an old fortress and begin learning about the region's vampire legends. They hear stories about the Bloodstone, King Vladislav and an annual Festival of the Undead. The villagers do not treat these stories as distant entertainment. Their customs, celebrations and fears suggest that folklore is still part of ordinary life.</p>
<p>The students also meet Stefan, a young zoologist who appears gentle, educated and strangely comfortable with nocturnal animals. Michelle and Stefan become attracted to each other, but he is hiding a rather substantial family detail.</p>
<p>Stefan is a vampire.</p>
<p>More specifically, he is Radu's half-brother. Unlike Radu, Stefan rejects the cruelty associated with his nature and tries to live without hunting human beings. He represents the possibility that a vampire is not automatically condemned to become a monster. Radu, naturally, finds this offensive.</p>
<p>The conflict between the brothers soon reaches the three women. Radu begins stalking them, attacks Lillian and later takes Mara. He wants them as consorts, but he also understands that harming Michelle will hurt Stefan. For Radu, desire and revenge are almost the same emotion.</p>
<p>Lillian becomes ill after the attack and eventually dies, only to rise again as a vampire under Radu's influence. Mara suffers a similar fate. Michelle, meanwhile, discovers Stefan's true identity and becomes trapped between the two brothers: one wants to protect her, while the other wants to corrupt everything his brother loves.</p>
<p>The final confrontation takes place at the castle. Radu holds Stefan and the women captive, intending to claim Michelle in front of him. Karl, a local man who understands the old vampire traditions, joins the attempt to stop Radu. The battle is chaotic, physical and appropriately Gothic. Radu is staked and beheaded, while Lillian and Mara cannot be restored to their former lives.</p>
<p>But Radu has already bitten Michelle.</p>
<p>To prevent her from becoming like Radu, Michelle asks Stefan to turn her himself. The film closes with Stefan and Michelle together in his coffin, apparently safe, while Radu's small servants gather around the remains of their master.</p>
<p>The evil is defeated.</p>
<p>Temporarily.</p>
<h2 id="radu-is-the-reason-the-film-survives">Radu is the reason the film survives</h2>
<p>I enjoy the locations, the folklore and the strange little title creatures, but Anders Hove's Radu is the image that remains after the film ends.</p>
<p>His performance is wonderfully excessive. Radu does not enter a room like a normal person. He leaks into it. His body bends forward, his hands seem too long for him, and every word sounds as though it has been pulled through a throat full of old blood.</p>
<p>The character owes something to the tradition of Count Orlok in <em>Nosferatu</em>, naturally, but Hove does not play him as a silent copy. Radu speaks, complains, threatens, envies and enjoys his own cruelty. There is something almost childish beneath the monster: an exiled son returning home to demand what he thinks should always have been his.</p>
<p>That combination makes him more memorable than a simple animal.</p>
<p>Radu is grotesque, but he has grievances.</p>
<p>Hove later explained that the voice was invented almost spontaneously. During filming, Ted Nicolaou asked him how the character should speak, and the actor produced the now-familiar voice on the spot. When Hove later suggested changing it for the sequels, Nicolaou refused. He was stuck with it.</p>
<p>Fortunately, so were we.</p>
<h2 id="romania-does-half-the-special-effects">Romania does half the special effects</h2>
<p>The smartest decision in <em>Subspecies</em> was filming in Romania.</p>
<p>Many inexpensive horror movies try to manufacture an ancient world by pointing smoke machines at a few artificial stones. This film did not need to pretend quite so much. It had real fortifications, real forests, old walls and a landscape carrying its own history.</p>
<p>The production arrived during an extraordinary moment. Filming took place shortly after the fall of Nicolae Ceaușescu's regime, when Romania was moving out of decades of communist dictatorship. Academic discussion of the film identifies it as the first vampire movie shot in post-communist Romania, while it is also widely credited as the first American production filmed in Bucharest after that political transformation.</p>
<p>That context matters because the scenery does not look like a carefully controlled tourist version of Transylvania. It feels unsettled. The villages, cemeteries and ruins give the film a scale that its budget alone could never have purchased.</p>
<p>Anders Hove remembered that the crew expected to stay for roughly four or five weeks but remained for about fourteen. Production was repeatedly affected by the unstable conditions of the period. He also recalled a huge hotel with space for hundreds of guests occupied by only their small crew.</p>
<p>That sounds lonely.</p>
<p>It also sounds exactly like a Full Moon vampire film.</p>
<h2 id="the-tiny-creatures-are-strange-unnecessary-and-perfect">The tiny creatures are strange, unnecessary and perfect</h2>
<p>Radu can create little demonic servants, the Subspecies of the title, from pieces of his own fingers. They crawl around, obey commands and eventually prove that a loyal employee can be useful even when he is only several inches tall and animated one frame at a time.</p>
<p>The creatures were realized through stop-motion effects associated with David Allen's visual-effects team. They do not appear constantly and, honestly, the central story could survive without them. The title promises more Subspecies than the film actually delivers.</p>
<p>I do not care.</p>
<p>Their limited presence makes the movie stranger. They feel as if they escaped from another Full Moon production, wandered into Radu's castle and received immediate employment. They also become important in the final image, gathering around Radu and preparing the continuation.</p>
<p>The film knows it has a sequel before the audience has finished the first one.</p>
<p>That confidence is admirable.</p>
<h2 id="the-makeup-was-a-small-daily-nightmare">The makeup was a small daily nightmare</h2>
<p>Radu's face and hands are essential to the character, but wearing them was apparently not pleasant. Hove said the makeup application took approximately three to four hours, followed by another two hours to remove it after a day of filming.</p>
<p>Think about that for a moment.</p>
<p>Before Radu could stalk anyone through a castle, the actor had already spent half a working day becoming Radu. After the final scene, he still had to sit for hours while the vampire was removed from him piece by piece.</p>
<p>Hove said red wine helped him through the removal process.</p>
<p>Reasonable.</p>
<p>The effort appears on screen. Radu does not look like an actor with pale foundation and plastic teeth. The makeup changes the architecture of his face. Combined with the fingernails, hair, posture and voice, it creates a complete silhouette. You can recognize him in shadow.</p>
<p>That is good monster design.</p>
<h2 id="what-does-not-work">What does not work</h2>
<p>The film has limitations.</p>
<p>Some performances are stiff. The romance between Michelle and Stefan develops more because the story requires it than because the actors generate an irresistible connection. The pacing can be slow, and the students occasionally behave with the relaxed curiosity of people who do not realize they have entered a vampire movie.</p>
<p>The plot is also simple. There is an evil brother, a good brother, a magical relic and a group of people placed between them. The film is not hiding an elaborate puzzle.</p>
<p>But simplicity is not the same as emptiness.</p>
<p>The story gives the locations room to breathe and gives Radu time to establish himself. The weaknesses even contribute to the peculiar tone. <em>Subspecies</em> feels less like a modern studio product and more like a forgotten regional legend reconstructed by a film crew that had limited money, great locations and one extraordinary vampire.</p>
<p>I would rather watch a movie with visible limitations and a real identity than a technically perfect film with nothing in its blood.</p>
<h2 id="why-i-liked-it-so-much">Why I liked it so much</h2>
<p>I liked <em>Subspecies</em> because it takes vampires seriously without becoming respectable.</p>
<p>It embraces castles, blood relics, festivals, coffins, ancient family disputes and creatures born from severed fingers. There is no embarrassment about being a Gothic vampire film. At the same time, it has the handmade energy of early Full Moon productions: practical makeup, stop-motion monsters, ambitious locations and a story constructed to continue beyond the final frame.</p>
<p>Most importantly, it gave horror a genuinely memorable vampire.</p>
<p>Radu is not beautiful. He is not misunderstood in a convenient romantic way. He is jealous, theatrical, disgusting and completely committed to his own importance. The film becomes alive whenever he appears.</p>
<p>That is enough for me to forgive quite a lot.</p>
<h2 id="my-rating-810">My rating: 8/10</h2>
<p><em>Subspecies</em> is not an 8/10 because every performance works, every effect convinces or every scene moves perfectly.</p>
<p>It is an 8/10 because it creates a world.</p>
<p>It uses Romania as more than a background, introduces a villain who could carry an entire franchise and understands the pleasure of an unapologetic vampire story. Its roughness is visible, but so is its imagination.</p>
<p>Some films have more money.</p>
<p><em>Subspecies</em> has Radu walking through a real Transylvanian ruin with the Bloodstone between his impossible fingers.</p>
<p>I know which one I would rather watch.</p>
]]></content:encoded>
    </item>
    <item>
        <title>Copyparty Is the File Server I Did Not Know I Needed</title>
        <link>https://pablomurad.com/copyparty-is-the-file-server-i-did-not-know-i-needed/</link>
        <guid isPermaLink="true">https://pablomurad.com/copyparty-is-the-file-server-i-did-not-know-i-needed/</guid>
        <pubDate>Sat, 18 Jul 2026 22:11:42 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>loveletter</category>
        <description>A practical love letter to a portable file server that keeps surprising me.

You know that rare kind of software that solves the problem you installed it for, then quietly reveals that it can solve another five?

Well, I think I found one.

I have been using copyparty, and I</description>
        <content:encoded><![CDATA[<p><em>A practical love letter to a portable file server that keeps surprising me.</em></p><p>You know that rare kind of software that solves the problem you installed it for, then quietly reveals that it can solve another five?</p><p>Well, I think I found one.</p><p>I have been using <a href="https://github.com/9001/copyparty">copyparty</a>, and I am honestly loving it. At first, the idea sounded almost too simple: run a program, point it at a directory, open a browser, and there are your files. But copyparty is one of those projects where “simple” describes the entrance, not the building.</p><p>Behind that browser window is a remarkably capable file server. It can handle resumable uploads, searchable indexes, duplicate detection, user accounts, per-folder permissions, media previews, WebDAV, SFTP, FTP and several other useful things. It runs on an absurd variety of systems. It can be a quick file drop, a personal web drive, a media shelf, a bridge between old machines or the front end of a much larger storage setup.</p><p>And somehow it still feels light.</p><h2 id="the-first-thing-i-loved-almost-no-ceremony">The first thing I loved: almost no ceremony</h2><p>There is a particular exhaustion that comes with self-hosted software. Sometimes you want one useful service and receive, as a bonus, a database, a cache, an identity platform, six containers and a small career in YAML maintenance.</p><p>Copyparty goes in the opposite direction.</p><p>The server itself only needs Python; its additional dependencies are optional and unlock extra features. The official project provides a self-contained Python version, a Python zipapp, a Windows executable and a Docker image. It can run on Linux, Windows, macOS, Android, iOS and a rather entertaining list of less common architectures and operating systems.</p><p>The fastest test is almost suspiciously easy:</p><pre><code class="language-bash">python copyparty-sfx.py
</code></pre><p>That is enough to turn the current directory into a browser-accessible file server. For a quick transfer on a trusted network, a temporary lab or an emergency involving two machines that refuse to cooperate, this is wonderful.</p><p>There is an important warning, however: running copyparty without arguments gives everyone who can reach it read and write access to the current folder. That default is convenient for testing, not a production security policy. Before exposing it beyond a trusted network, configure accounts, volumes and permissions, and place it behind properly configured HTTPS when appropriate.</p><p>Easy to start does not mean safe to forget.</p><h2 id="it-is-much-more-than-a-page-with-files">It is much more than a page with files</h2><p>The web interface is where copyparty stopped feeling like a clever transfer utility and started feeling like infrastructure I could actually keep.</p><p>From the browser, I can navigate directories, upload and download files, create folders, rename items, move or copy them and undo accidental uploads when the server allows it. Folders can be downloaded as ZIP or TAR archives. There is a navigation pane, keyboard shortcuts, thumbnails, an image gallery, a Markdown viewer and even media-oriented features such as audio playback, playlists and server-side transcoding when the optional tools are available.</p><p>This matters because a file server should not require every person to understand a file server.</p><p>Give someone a clean web address and the interaction is already familiar. They do not need to mount a network share. They do not need an FTP client. They do not need to learn which operating system is running on the other side. A browser is enough.</p><p>But if a browser is not enough for your workflow, copyparty also speaks WebDAV, SFTP, FTP, TFTP and SMB/CIFS, depending on configuration and platform support. It can announce services on a local network through Zeroconf, mDNS and SSDP. The same collection of files can therefore meet modern browsers, desktop file managers, command-line tools and old machines without forcing everything through one narrow door.</p><p>That flexibility is not decorative. It is useful.</p><h2 id="resumable-uploads-are-the-feature-you-appreciate-after-something-fails">Resumable uploads are the feature you appreciate after something fails</h2><p>Large uploads are easy to advertise when the connection is perfect. Real networks are less polite.</p><p>Copyparty's main browser uploader, called <strong>up2k</strong>, supports resumable and multithreaded transfers. If an upload is interrupted, the useful response is not to punish the user by starting again from zero. The server and browser can continue the work.</p><p>This is one of those features that sounds technical until you need it. Then it becomes the entire reason you trust the software.</p><p>It also supports write-only folders, filename randomization, self-destructing uploads and content-based duplicate handling. A write-only volume is especially interesting for collecting files: people can submit material without receiving permission to browse everything that other people have uploaded. With the right volume permissions, copyparty can behave like a private drop box instead of a public cupboard.</p><h2 id="search-indexing-and-the-quiet-intelligence-of-deduplication">Search, indexing and the quiet intelligence of deduplication</h2><p>Enable file indexing with <code>-e2dsa</code>, and copyparty can scan the shared volumes and make them searchable from the web interface. Searches can use names, paths, dates and sizes. With media indexing enabled, metadata such as audio tags can also become part of the search.</p><p>The clever part is that indexing is connected to duplicate detection. Copyparty can compare content and avoid storing the same upload repeatedly. You can even drag a local file into the search interface to check whether identical content already exists somewhere on the server.</p><p>This changes how the server feels as a collection grows. It is no longer only a directory exposed over HTTP. It begins to understand enough about the collection to help you navigate it.</p><p>Not everything needs artificial intelligence.</p><p>Sometimes a good index is the intelligent thing.</p><h2 id="accounts-and-volumes-make-it-practical">Accounts and volumes make it practical</h2><p>The permission model is another reason I can imagine copyparty serving very different roles without becoming a mess.</p><p>A <strong>volume</strong> maps a real directory to a location in the web interface. Each volume can have its own access rules, and permissions can be assigned to users. One folder may be public and read-only. Another may allow uploads but hide its contents. A private directory may be visible only to one account, while an administrator receives broader file-management permissions.</p><p>This separation is simple enough to understand and flexible enough to matter. I can think in terms of actual use:</p><ul><li>a public download area;</li><li>a private family archive;</li><li>an upload-only inbox;</li><li>a music library with browser playback;</li><li>a working directory available through WebDAV;</li><li>a temporary share with limited permissions.</li></ul><p>One service. Different doors. Different keys.</p><p>For longer-lived installations, the project recommends using a configuration file rather than accumulating a heroic command line. The official example shows global options, accounts and volumes in one readable file, and account or volume changes can be reloaded without restarting the entire service in supported setups.</p><h2 id="it-respects-small-machines">It respects small machines</h2><p>Perhaps this is the part I like most.</p><p>Copyparty does not begin with the assumption that every useful service deserves a new server. Its project philosophy is wonderfully direct: run anywhere, support everything, require little preparation and keep dependencies minimal.</p><p>That makes it useful on a home server, naturally. But it also makes it interesting on an old laptop, a small single-board computer, a temporary virtual machine or some forgotten device that still has a disk, a network connection and enough life left to be useful.</p><p>Software like this gives hardware a second purpose.</p><p>And there is something deeply satisfying about that. We are surrounded by machines that are declared obsolete long before they become incapable. A portable file server will not solve electronic waste, naturally, but it can turn an idle computer into a useful point of exchange again. Sometimes the difference between junk and infrastructure is one good piece of software.</p><h2 id="docker-when-i-want-it-plain-python-when-i-do-not">Docker when I want it, plain Python when I do not</h2><p>I also appreciate that copyparty does not turn deployment preference into a religion.</p><p>If I want the direct route, I can run the standalone Python package. If I want a container, the project maintains the <code>copyparty/ac</code> image and provides Compose examples. If I am on Windows, there is an executable. If I want to integrate it with a service manager, reverse proxy or a more carefully designed storage layout, the configuration is there.</p><p>This is how portable software should behave. It offers choices without making every choice mandatory.</p><p>For a serious deployment, I would still treat it like any other internet-facing service: use a dedicated account, expose only the directories that are necessary, grant the smallest useful permissions, keep the software updated, use HTTPS, understand the reverse-proxy headers and review the project's hardening guidance. Anonymous uploads deserve additional care, particularly around browser-rendered HTML, SVG and Markdown content.</p><p>Copyparty makes the first ten minutes easy. Security remains our job after that.</p><h2 id="why-i-am-enjoying-it-so-much">Why I am enjoying it so much</h2><p>There are larger platforms. There are more polished cloud suites. There are systems with calendars, collaborative documents, contact synchronization and enough plugins to recreate an office building inside a browser.</p><p>Copyparty is not trying to become all of that.</p><p>It is trying to move, organize, expose, search and play files extremely well across an unreasonable number of environments. Its own documentation jokes about following an “inverse Unix philosophy”: do all the things, and do an okay job. The joke is fair, but it hides the impressive part. The features do not feel like a random pile. They orbit the same practical question:</p><p>How can I make these files useful from another device?</p><p>That question appears simple. It is not. Different users, unreliable uploads, old protocols, browsers, media, permissions, duplicate data, reverse proxies and strange operating systems all arrive eventually. Copyparty has apparently met most of them already.</p><p>And this is why I am loving it.</p><p>It does not demand that I build my life around the software. It arrives, points at the files and becomes useful. Then, when I need more, there is usually another switch, another protocol or another carefully strange feature waiting in the documentation.</p><p>Some software wants to be a platform.</p><p>Copyparty wants to be invited to the party, carry every box through the door and make sure nothing gets lost on the way.</p><p>Honestly, it can stay.</p><h2 id="useful-official-links">Useful official links</h2><ul><li><a href="https://github.com/9001/copyparty">Copyparty on GitHub</a></li><li><a href="https://copyparty.eu/">Official project website and downloads</a></li><li><a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/example.conf">Example configuration</a></li><li><a href="https://github.com/9001/copyparty/blob/hovudstraum/docs/examples/docker/basic-docker-compose/docker-compose.yml">Basic Docker Compose example</a></li></ul>]]></content:encoded>
    </item>
    <item>
        <title>How I built a public fax wall</title>
        <link>https://pablomurad.com/how-i-built-a-public-fax-wall/</link>
        <guid isPermaLink="true">https://pablomurad.com/how-i-built-a-public-fax-wall/</guid>
        <pubDate>Sat, 18 Jul 2026 15:47:00 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>papers</category>
        <description>A real fax number, a static website, and no application server. Here’s how I built an automated public fax wall using Gmail, Apps Script, Drive, Sheets, Cloudinary, cron, and nginx.</description>
        <content:encoded><![CDATA[<p><a href="https://fax.1208.pro"><strong>https://fax.1208.pro</strong></a></p><p>There is a working fax number out in the world and it belongs to me. If you send a page to it, that page may end up hanging on a web page, in black and white, for any stranger to look at. No signup, no login, no app. Just a phone number and an old machine on the other end of an imaginary line.</p><p>The idea came out of a small grudge. "Just send the link" has become the beige wallpaper of modern life. Everything is instant, everything is editable, everything disappears. Fax isn't like that. Fax is stubborn, far too physical for 2026, and somehow still alive — running like a haunted appliance with union protection. I wanted to see what would happen if I left that channel open to the world and published whatever came through without much curation.</p><p>This is about how the thing was built. I'll walk through each piece in the order a sheet of paper travels through the system.</p><h2 id="the-rule-that-shaped-the-architecture">The rule that shaped the architecture</h2><p>Before the first file existed, I had one personal constraint: <strong>no application server</strong>.</p><p>No backend running around the clock, no database to back up, no container to patch at three in the morning because somebody published a CVE. This is a toy project. If it demands maintenance, it dies in three months. They all do.</p><p>So the decision was: the final site is <strong>static HTML</strong>. One file. Nginx serves it and that's the end of it. Everything dynamic happens far away, on infrastructure somebody else maintains for free, and the result lands as a dumb JSON file on disk.</p><p>That sounds like laziness. It is, but it's laziness with a design behind it. Every other choice in the system falls out of this rule.</p><h2 id="piece-1-getting-a-real-fax-number">Piece 1: getting a real fax number</h2><p>I don't have a phone line and I'm not buying a machine. I used a fax-to-email service — fax.plus, in this case. You rent a number, and when somebody transmits to it, the service answers the call, decodes the signal, and emails you the document as an attachment, PDF or TIFF.</p><p>The number is published on the site, because that's the entire point: <strong>+1 762 475 9826</strong>.</p><p>Here's the trick that made the project viable: the moment a fax becomes an email with an attachment, it stops being telephony and becomes a Gmail inbox. And a Gmail inbox is something I know how to automate without paying anyone.</p><h2 id="piece-2-google-apps-script-playing-the-part-of-a-backend">Piece 2: Google Apps Script playing the part of a backend</h2><p>The whole core of the system lives in a single <code>Código.gs</code> file running on Google Apps Script. It's JavaScript hosted by Google with native access to Gmail, Drive, and Sheets, time-based triggers, and a public HTTP endpoint if you want one. It costs nothing and I administer none of it.</p><p>It has two main functions that run in sequence.</p><h3 id="ingestfaxes-%E2%80%94-fishing-out-the-new-faxes"><code>ingestFaxes()</code> — fishing out the new faxes</h3><p>The function sweeps Gmail with a specific query: messages from the fax service's notification sender, within the last 30 days. The search is paginated 100 threads at a time in a loop, because the Apps Script API hands results back in batches, and ignoring that is the classic way to silently lose older messages once volume grows.</p><p>For each message, three decisions:</p><p><strong>Have I seen this one?</strong> Before anything else, I compare the Gmail message ID against the IDs already on record. This is the heart of the system's idempotency. The trigger fires periodically and always looks at a rolling 30-day window, which means it reprocesses the same messages dozens of times. Without a reliable dedup key, the wall would turn into a wall of duplicates. Using the message ID rather than the subject or the date was the right call: subjects repeat, dates collide, IDs don't.</p><p><strong>Which attachment is the fax?</strong> A notification email arrives with a logo, a footer, an inline image, sometimes a receipt. I wrote a function that filters for plausible candidates — <code>.pdf</code>, <code>.tif</code>, <code>.tiff</code>, or the matching content types — and then scores the survivors: PDF is worth 3, TIFF is worth 2, and file size enters as a tiebreaker divided by ten million. Highest score wins.</p><p>There's one ugly case in there that I left on purpose: if an attachment shows up as <code>application/octet-stream</code> and is larger than 1 KB, I accept it. Fax gateways are notorious for sending generic content types. Rejecting on technical purity would mean dropping legitimate faxes, and the cost of the opposite error is low — worst case, Cloudinary refuses the file downstream and the row simply ends up without an image.</p><p><strong>Keep the original.</strong> The chosen attachment is copied into a Drive folder under a deterministic name: <code>fax_20260410_143022_&lt;messageId&gt;</code>. That's the raw file, untouched. If I break every other stage of the pipeline, the source material is still sitting there.</p><p>Then a row goes into the spreadsheet with its own UUID, the received date, the sender, the subject, the Drive file ID, the attachment name and type, and a short public slug derived from the first eight characters of the UUID.</p><h3 id="the-spreadsheet-as-a-database">The spreadsheet as a database</h3><p>Yes, the database is a fifteen-column Google Sheet. I recommend this far more often than people expect for projects this size. It comes with a free admin interface, revision history, permissions, and export, and I can fix a bad record from my phone on the bus without touching SSH.</p><p>One thing I did that saved me pain: a function that <strong>checks and repairs the sheet's header row every single time the script runs</strong>. It verifies the fifteen columns are where they should be, inserts columns if any are missing, and rewrites the header row if it doesn't match. A spreadsheet is human-editable, and humans drag columns around. Assuming the schema is intact is optimism. The code never reads a column by fixed position — it always builds a name-to-index map from row 1.</p><p>Every row is born with status <code>approved</code>. The schema supports moderation — the field is there, and the publishing function only looks at approved rows — but in practice the gate is wide open. That was a deliberate choice about the spirit of the site, not an oversight. The switch exists for the day I need it.</p><h3 id="publishapproved-%E2%80%94-turning-a-pdf-into-a-publishable-image"><code>publishApproved()</code> — turning a PDF into a publishable image</h3><p>Second function. It looks for approved rows that don't have a published image yet, pulls the original file from Drive, and pushes it to Cloudinary under a predictable public ID.</p><p>And this is where my favorite part of the whole project lives, because it's work I <strong>didn't</strong> have to do.</p><p>The real problem was: how do you turn a fax PDF into an image that fits on a web page? The obvious answer involves rasterizing PDF, which means ImageMagick or Ghostscript, which means a server, which breaks rule number one.</p><p>Cloudinary does it in the URL. After the upload, the public image is assembled from four transformations chained into the path:</p><ul><li><code>pg_1</code> — take only the first page of the PDF</li><li><code>dn_200</code> — rasterize at 200 DPI, fax density</li><li><code>e_blackwhite</code> — force pure black and white, no halftone</li><li><code>q_auto</code> — let Cloudinary pick the compression</li></ul><p>No processing happens on my side at all. I upload the raw PDF and request an image by URL. The conversion runs on their CDN, on demand, and gets cached. The result looks exactly the way I wanted — fax grain, high contrast, none of that tasteful gray.</p><p>The image URL and the public ID go back into the spreadsheet, and the row is live.</p><h2 id="piece-3-privacy-or-at-least-the-facade-of-it">Piece 3: privacy, or at least the facade of it</h2><p>The wall displays pages that strangers sent me. I have no idea what's going to arrive. So there's a hygiene layer working at two levels.</p><p>On the server side, the sender runs through a function that tries to extract a human name from the email's <code>From</code> field. It handles the <code>"Some Person" &lt;person@domain.com&gt;</code> format, strips outer quotes, and then applies two rejection tests: if what's left looks like an email address, discard it; if it looks like a phone or fax number — seven or more digits, nothing but phone characters — discard it. Anything suspicious becomes <code>Unknown sender</code>.</p><p>This matters more than it looks. Fax carries the originating number in its header, and the service frequently drops that number straight into the sender field. Publishing it would mean leaking the phone number of whoever sent me a joke.</p><p>On the client side I went further: <strong>the page doesn't render the sender name at all</strong>. The public JSON carries the field, the front-end simply never draws it. Every entry on the wall shows the date, the image, and an optional note. That's it. As the site's own FAQ puts it — the machine knows, obviously, but it is not a snitch.</p><h2 id="piece-4-how-the-data-leaves-google-and-reaches-my-vps">Piece 4: how the data leaves Google and reaches my VPS</h2><p>The Apps Script publishes a Web App with three output formats, selected by URL parameter: a rough HTML gallery, <code>format=json</code>, and <code>format=rss</code>. The last two are the ones that matter. Both accept a per-item filter and a count limit.</p><p>Now, I could have just pointed the site at Google's endpoint and called it done. I didn't, for three reasons: the URL is ugly and advertises the implementation, Apps Script has execution quotas, and I don't want my site's availability to depend on a Google redirect resolving during a visitor's request.</p><p>So there's an eighty-line shell script running hourly on the VPS via cron. It's small, but there are four deliberate decisions inside it.</p><p><strong>Cascading configuration.</strong> The script loads config files in order — repository first, then <code>~/.config</code> — and environment variables override everything. That gives me versioned defaults plus machine-local overrides without a single <code>if</code>.</p><p><strong>Explicit failure.</strong> It opens with <code>set -euo pipefail</code> and aborts with a clear message if the Web App URL isn't configured. A silent cron job is the worst category of bug: the site just freezes in time and nobody notices for three weeks.</p><p><strong>URL rewriting.</strong> Every link pointing at Google's domain inside the payload gets swapped for the public domain, using <code>perl</code> with both ends passed in through environment variables instead of interpolated into the regex. That keeps a slash or a question mark in the URL from turning into a metacharacter and destroying the file.</p><p><strong>Atomic writes.</strong> This is the most important one and the easiest to forget. <code>curl</code> downloads to <code>faxes.json.tmp</code>, and only then does <code>mv</code> move it over the real file. <code>mv</code> within the same filesystem is an atomic rename, which means nginx never — not for a single instant — serves a half-written JSON. If you download straight over the file being served, sooner or later a visitor catches the transfer mid-flight and gets a parse error.</p><p>Nginx serves both files with <code>Cache-Control: max-age=3600</code>, deliberately aligned to the cron interval. There's no point caching for longer than the refresh, or for shorter.</p><h2 id="piece-5-the-page">Piece 5: the page</h2><p>The front-end is a single HTML file. No build step, no framework, no dependencies, no <code>npm install</code>. It loads the JSON with <code>fetch</code> and assembles the list.</p><p>The look is monospace, light gray background, blue underlined links in the browser's original default. That's not aesthetic laziness — that <em>is</em> the aesthetic. A fax wall shouldn't look like a SaaS product.</p><p>A few details worth mentioning:</p><p>The favicon is a fax emoji embedded as an SVG data URL. Zero extra requests, no <code>.ico</code> file.</p><p>The image containers have <code>aspect-ratio: 210 / 297</code> locked in before any image loads. That's the A4 ratio. The space is reserved at the correct size up front, so nothing jumps around when the images arrive.</p><p>The first image loads with <code>loading="eager"</code> and <code>fetchpriority="high"</code>; every other one is <code>lazy</code>. There's a <code>preconnect</code> to Cloudinary's domain in the <code>head</code>, so the TLS handshake is already underway before the JSON finishes downloading.</p><p>The JSON parser is intentionally forgiving: if the response is an array, use it; if it's an object, look for <code>items</code>, <code>faxes</code>, <code>records</code>, <code>data</code>, or <code>entries</code>. It cost six lines and lets me change the backend's shape without breaking the live page.</p><p>And everything coming out of the JSON gets HTML-escaped before it touches the DOM. I am rendering content that anonymous strangers transmitted over a phone line. Concatenating that straight into <code>innerHTML</code> would be an open invitation for the first clever visitor to write the rest of my site for me.</p><h2 id="piece-6-the-doorman">Piece 6: the doorman</h2><p>Sitting in front of all of it is Anubis, a proxy that demands proof-of-work from the browser before it lets the page through. It's there because AI training crawlers started burning through small-VPS bandwidth as if it were infinite.</p><p>I found out it was stricter than I remembered when I tried to fetch my own site with an automated tool and got an "access denied" to the face. Working as designed, I suppose.</p><h2 id="the-whole-flow-in-one-line">The whole flow, in one line</h2><p>Somebody dials the number → the service converts the call into an email with an attachment → Apps Script finds the email, dedupes on the message ID, picks the right attachment, archives the original in Drive, and writes a row to the spreadsheet → it uploads the PDF to Cloudinary and requests page one in black and white at 200 DPI → it publishes a JSON feed → the VPS cron pulls that JSON hourly and atomically swaps it over the old one → nginx serves it → static HTML draws it.</p><p>Not one process of mine runs continuously. The most complex part of the system — rasterizing PDF — is a parameter in a URL. The part that most resembles a database is a spreadsheet I can open on my phone.</p><h2 id="what-id-do-differently">What I'd do differently</h2><p>Two things bother me when I reread the repository.</p><p>First: the Web App URL is committed to the environment file inside the repo, and the README itself says it should live outside of it. It's a read-only endpoint, so it isn't catastrophic, but it's exactly the kind of thing you swear you'll fix later and never do. The right move is to purge it from history, publish a new Web App version, and keep the value only in <code>~/.config</code> on the VPS.</p><p>Second: the <code>data/faxes.json</code> sitting in the repo is empty. It's a placeholder — the real file is generated on the VPS by cron — but a committed empty file is a trap waiting for someone to deploy it over the good one.</p><p>Beyond that, I'd keep all of it. Especially the part where there's no server to maintain.</p>]]></content:encoded>
    </item>
    <item>
        <title>Movie: A Serious Man</title>
        <link>https://pablomurad.com/movie-a-serious-man/</link>
        <guid isPermaLink="true">https://pablomurad.com/movie-a-serious-man/</guid>
        <pubDate>Sat, 18 Jul 2026 09:15:11 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>movies</category>
        <description>A Serious Man turns spiritual confusion, domestic collapse, and cosmic indifference into razor-sharp black comedy. The Coens offer no answers—only the terrifying possibility that meaning exists, but remains beyond our reach.</description>
        <content:encoded><![CDATA[<p><em>A Serious Man</em> is one of the funniest films I have ever watched, although “funny” feels like an inadequate word for what the Coen brothers are doing here. This is not comedy designed to make you comfortable. It is comedy extracted from humiliation, spiritual confusion, domestic collapse and the quiet suspicion that the universe may be operating according to rules no one bothered to explain to us.</p><p>I loved it.</p><p>The film follows Larry Gopnik, a physics professor living in Minnesota in 1967, whose life begins to fall apart with almost mathematical precision. His wife wants a divorce so she can marry Sy Ableman, a man so calm, reasonable and unbearably sympathetic that his politeness becomes a form of violence. Larry’s brother is sleeping on the couch, draining a cyst in the bathroom and working obsessively on a mysterious notebook. His children treat him like an inconvenient financial institution. A student tries to bribe him and then threatens to sue him. His tenure is uncertain. His roof needs repairing.</p><p>And Larry, poor bastard, keeps asking the same question:</p><p>Why?</p><p>That is the real joke.</p><p>Larry is a man of science. He teaches uncertainty, probability and the limitations of human knowledge, but he cannot tolerate uncertainty when it enters his own house. He can explain Schrödinger’s cat on a blackboard, yet he cannot understand why his wife suddenly prefers Sy Ableman. He wants the universe to present its calculations. He wants suffering to show its work.</p><p>But the universe does not care about intellectual consistency.</p>
<!--kg-card-begin: html-->
<iframe title="A Serious Man" width="560" height="315" src="https://pablo.tube/videos/embed/gB3fiKaM1kACUzaFBQLpyH" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
<!--kg-card-end: html-->
<p><em>A Serious Man</em> is essentially the Book of Job relocated to a Jewish suburb, except that God never arrives to explain Himself. There are rabbis, lawyers, doctors, neighbors and academic committees, but no authority capable of giving Larry a useful answer. Every person he consults offers either a meaningless story, a bureaucratic procedure or some variation of “accept the mystery.”</p><p>The rabbis are especially brilliant. Larry approaches them expecting ancient wisdom and receives anecdotes, platitudes and administrative delays. One rabbi tells him a strange story about a dentist who discovered Hebrew letters carved into a patient’s teeth. The story sounds as though it must contain a profound revelation. Naturally, it leads nowhere.</p><p>That is exactly the point.</p><p>Human beings are addicted to the idea that everything means something. We cannot accept random suffering, so we invent structures around it. Religion, mathematics, superstition, philosophy, bureaucracy — all of them become methods of drawing borders around the incomprehensible. We ask for signs, and when a sign appears, we immediately ask what the sign means.</p><p>Maybe it means nothing.</p><p>Maybe the teeth are just teeth.</p><p>The humor in <em>A Serious Man</em> is brutally precise. The Coens never beg for laughter. They simply allow situations to become so uncomfortable, so absurdly unfair, that laughter becomes the only honest reaction. Sy Ableman embracing Larry and telling him how difficult the divorce must be is funnier than any conventional joke. He steals the man’s wife and then comforts him about it. It is an almost perfect portrait of passive aggression disguised as compassion.</p><p>Fred Melamed plays Sy with a kind of majestic softness. Every word sounds therapeutic. Every gesture feels murderous.</p><p>Michael Stuhlbarg is equally extraordinary as Larry. His performance is built from confusion, suppressed panic and the exhausted decency of a man who still believes that behaving correctly should protect him from catastrophe. Larry is not heroic, but he is recognizably human. He does not demand happiness. He merely wants an explanation.</p><p>He does not get one.</p><p>What makes the film even better is that it refuses to turn Larry into a saint. He is passive, indecisive and frequently blind to the people around him. He spends so much time asking what God wants from him that he rarely considers what anyone else might need. His suffering is real, but so is his self-absorption.</p><p>This prevents the film from becoming a simple story about an innocent man punished by fate. Larry may not deserve what happens to him, but the universe has never been particularly interested in proportional punishment.</p><p>The cinematography is controlled, clean and almost clinical. Everything in Larry’s world appears organized: suburban houses, synagogue rituals, university offices, mathematical formulas. Yet beneath that order is complete chaos. The visual neatness makes the existential disorder even funnier. The furniture is aligned. The lawns are trimmed. God remains unavailable.</p><p>Then there is the ending.</p><p>No sentimental resolution. No final rabbinical wisdom. No comforting proof that suffering produces growth. Larry makes one small moral compromise, the doctor calls with troubling news, and a tornado approaches his son’s school while Jefferson Airplane plays.</p><p>The film ends not with an answer, but with an interruption.</p><p>It is perfect.</p><p>The tornado is not merely punishment. That interpretation would be too easy, and <em>A Serious Man</em> distrusts easy interpretations. It is uncertainty made visible — enormous, indifferent and moving directly toward everyone. The children stare at it because there is nothing else to do.</p><p>That final image stayed with me because it expresses the whole film without pretending to solve it. We live inside systems we barely understand. We make plans, study sacred texts, calculate probabilities, repair antennas and worry about television reception while something immense forms on the horizon.</p><p>And still, somehow, it is hilarious.</p><p><em>A Serious Man</em> is not nihilistic, although it frequently looks in that direction. Nihilism would be simpler. The film’s position is more disturbing: meaning may exist, but we may be fundamentally incapable of accessing it. God may have a plan. God may be absent. God may simply have a very strange sense of humor.</p><p>The Coen brothers do not answer the question.</p><p>They understand that the question is funnier.</p>]]></content:encoded>
    </item>
    <item>
        <title>No Legacy, Just Curiosity</title>
        <link>https://pablomurad.com/no-legacy-just-curiosity/</link>
        <guid isPermaLink="true">https://pablomurad.com/no-legacy-just-curiosity/</guid>
        <pubDate>Sat, 18 Jul 2026 00:43:35 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>life</category>
        <description>A reflection on learning without fame, driven by curiosity, persistence, and the joy of understanding—while building friendships and a world where everyone’s strangeness and beauty can belong.</description>
        <content:encoded><![CDATA[<p>I do not want fame, nor do I care about leaving a legacy. It makes little difference to me whether I am known or not. I simply want to learn and understand.</p><p>Many things are driven by hype, while others make us feel less excluded—or, more accurately, more included but I am not looking for any of that. In truth, I do not care what you are, as long as you have something to teach. I am here to learn from you, and I hope you can learn something from me as well, whether in programming or in management.</p><p>I am, by far, the worst programmer I know, despite having “played around” with Delphi since 1997. On the other hand, it would be equally true to say that I am good at management. But none of that really matters. What matters is that, in this space, we are driven by curiosity, by that sudden urge to keep going without giving up, and by the desire to see things work.</p><p>Curiosity is the force that drives us. It makes us dig deeper, leaving behind beautiful memories and nurturing new friendships (perhaps lifelong ones) each person with their own strangeness and beauty, exactly as they are. Together, we are building a world that adapts to us.</p>]]></content:encoded>
    </item>
    <item>
        <title>Movie: Napoleon Dynamite</title>
        <link>https://pablomurad.com/movie-napoleon-dynamite/</link>
        <guid isPermaLink="true">https://pablomurad.com/movie-napoleon-dynamite/</guid>
        <pubDate>Sat, 18 Jul 2026 00:26:23 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>movies</category>
        <description>A gloriously awkward comedy where tater tots, bad dancing, Uncle Rico’s delusions, and small-town weirdness become cinematic perfection. Napoleon Dynamite is deadpan, ridiculous, and impossible not to love.</description>
        <content:encoded><![CDATA[
<!--kg-card-begin: html-->
<iframe title="Napoleon Dynamite" width="560" height="315" src="https://pablo.tube/videos/embed/rSczyLKwquumEsY9FiyDva" style="border: 0px;" allow="fullscreen" sandbox="allow-same-origin allow-scripts allow-popups allow-forms"></iframe>
<!--kg-card-end: html-->
<p><strong>The Art of Absolutely Nothing Happening</strong></p><p>Napoleon Dynamite is a movie in which almost nothing happens - and, somehow, everything happens. There is no proper villain, no grand mission, nobody has to save the world, and no character stops the story to explain their trauma while staring through a rain-soaked window. There is just a teenager in moon boots walking around Preston, Idaho, looking like he was dragged out of a bad dream and told he has to present a group project.</p><p>That is exactly why I loved it. The film has the nerve to exist on its own frequency, a kind of small-town fever dream where every silence lasts half a second longer than it should, people talk with absolutely no sense of social rhythm, and a spoonful of mashed potatoes can carry more dramatic tension than the entire third act of most blockbusters. The humor does not come from traditional punchlines. It comes from the pause, the blank stare, the wrong outfit, the even-worse response, and the certainty that nobody in that town has the faintest idea how a human interaction is supposed to work.</p><p>It is deadpan elevated to a religion. The movie never begs for laughs, never uses the soundtrack to announce that something is funny, and never tries to win us over with artificially adorable characters. It simply shows Napoleon drawing a liger - "pretty much my favorite animal" - and moves on as though this were perfectly ordinary information. And maybe it is. After a few minutes, the movie's logic takes over. You stop wondering why its universe is so strange and begin to suspect that the real world is simply too conventional.</p><p>Napoleon is a walking masterpiece of awkward energy. He is irritating, arrogant, vulnerable, loyal, and completely incapable of understanding how other people see him. In a lazier high-school comedy, he would be the nerd who gets a makeover, learns how to use hair gel, and earns the approval of everyone who used to despise him. Not here. Napoleon remains Napoleon. There is no glow-up, no makeover montage, no speech about believing in yourself. There is only a boy who desperately wants everyone to know he has "nunchuck skills, bow hunting skills, computer hacking skills" - although the available evidence is, at best, inconclusive.</p><p>That is one of the sweetest things about the movie, even though it hides every trace of sentiment behind a bored expression. Napoleon does not need to become cool. Pedro does not need to become a charismatic candidate coached by consultants. Deb does not need to stop being shy. They only need to find some small way to exist together. The film looks at the weird kids without turning them into cute mascots, misunderstood geniuses, or moral lessons. They are strange, occasionally annoying, and frequently lost - and they still deserve friendship, dignity, and a place on the stage.</p><p>Pedro, by the way, is the true calm king of American cinema. While everyone else seems permanently one social interaction away from collapse, he moves through the film with the serenity of a man who has already realized that reality is far too ridiculous to justify anxiety. His campaign for class president is basically an anti-marketing performance: very few words, zero manufactured enthusiasm, and a slogan that entered pop culture because it cannot be improved. Vote for Pedro. That is it. That is the platform.</p><p><strong>Uncle Rico and the Multiverse of 1982</strong></p><p>If Napoleon is the teenager who has not yet found his place in the world, Uncle Rico is the adult who found his place in 1982 and refused to leave. He does not merely live in the past: he rented the past, furnished the past, and is probably trying to sell plastic containers to the past. Every time he appears, the movie gains another layer of discomfort. Rico has the confidence of a motivational speaker and the credibility of a man who records himself throwing a football beside a van.</p><p>His dream of traveling back in time to win a high-school football game is funny because it is absurd, but also because it is painfully recognizable. Everyone knows someone who turned one specific year of their youth into an entire personality. Uncle Rico is the patron saint of men who say, "I could have gone pro," while holding a warm beer at a barbecue. He permanently radiates "peaked in high school" energy, except for the minor detail that he may never have peaked at all. It is tragic, pathetic, and wonderful.</p><p>Kip, meanwhile, managed to become terminally online before it was recognized as a social diagnosis. He spends his days talking to women in chat rooms, discusses technology with the solemnity of a Silicon Valley pioneer, and somehow ends up in a genuinely sweet romance with LaFawnduh. The film could easily have used their relationship as nothing more than a cruel joke. Instead, it gives Kip perhaps the most sincere romantic arc in the entire story. In a universe where almost everyone seems emotionally frozen, the man who sings about technology finds somebody. Good for him, honestly.</p><p><strong>Tater Tots, Glamour Shots, and an Aesthetic You Cannot Fake</strong></p><p>Visually, Napoleon Dynamite looks like it was discovered inside a forgotten box in a basement, somewhere between a corporate training VHS and a 1987 school catalog. I mean that as a huge compliment. The faded colors, empty spaces, furniture, hairstyles, clothes, and endless Idaho sky create a place that seems trapped outside time. The film came out in 2004, but it could take place in 1986, 1994, or an alternate dimension where every store still sells binders with horses on the cover.</p><p>The direction understands that objects can tell jokes too. The tater tots hidden in Napoleon's pocket are not merely food; they are a manifesto. Deb's glamour shots, Uncle Rico's tapes, Pedro's bicycle, Tina the llama, the "delicious bass" - everything seems to have been selected by someone with an advanced degree in the science of embarrassment. Some movies spend millions building universes. Napoleon Dynamite needs only a corded telephone, a beige sofa, and a wolf T-shirt to establish an entire cosmology.</p><p>And then the dance happens. By that point, the movie has already proved it obeys none of the usual rules. But when Napoleon walks onto the stage and dances to Jamiroquai's "Canned Heat," the comedy becomes a strange little miracle. It is not technically perfect, nor should it be. It is awkward, intense, unexpected, and completely free. For the first time, Napoleon stops explaining his alleged skills and simply demonstrates one. The boy understood the assignment.</p><p>There is a huge difference between a comedy that humiliates its characters and a comedy that understands being human is already humiliating enough. Napoleon Dynamite almost always stays on the right side of that line. The movie laughs at its characters' social failures, of course, but rarely treats them with contempt. Even its most ridiculous people want things we can understand: Napoleon wants respect, Pedro wants to belong, Deb wants to be seen, Kip wants love, and Uncle Rico wants a time machine because accepting the present would be much harder.</p><p>Not everything will work for everyone. The story feels deliberately assembled from pieces somebody found scattered on the floor, the pace is so slow that some viewers will feel as though they are watching paint dry in Idaho, and several scenes end without the payoff a conventional comedy would promise. Anyone who needs plot twists, perfectly engineered character arcs, and a joke every fifteen seconds will probably hate it. Fair enough. But demanding conventional structure from this movie is like complaining that a liger does not exist in nature. You have completely missed the point.</p><p>For me, the pace was not an obstacle; it was the mechanism of the joke. I laughed so much because the film gives its strangeness room to breathe. Every silence becomes a trap. Every conversation sounds as though it was written by aliens who learned about humanity from high-school yearbooks. There is enormous precision behind the appearance of casual improvisation. The comedy looks effortless, but the timing is surgical.</p><p><strong>Vote for Pedro - and for the Right to Stay Weird</strong></p><p>In the end, Napoleon Dynamite is a comedy about people who do not know how to communicate, dress, flirt, or, in several cases, what they are doing at all. In other words, it is a movie about people. Its genius lies in turning small lives, monotonous routines, and social failures into something almost epic without ever abandoning the tone of someone answering, "Whatever I feel like I wanna do, gosh," when asked what they plan to do that day.</p><figure class="kg-card kg-image-card"><img src="https://pablomurad.com/content/images/2026/07/images.jpg" class="kg-image" alt="" loading="lazy" width="447" height="447"></figure><p>I finished the movie with that rare feeling of having visited an entire place. Not just a collection of characters, but a town, a temperature, a particular kind of carpet, an imaginary smell of a school cafeteria. It is extremely funny because it trusts the absurdity of everyday life and understands that the most memorable moments do not always arrive with a grand musical score. Sometimes they involve a student election, a drawing of a hybrid animal, a plate of nachos, or someone shouting at a llama to eat.</p><p>Napoleon Dynamite is weird cinema in its purest form: too specific to have been manufactured by committee, too sincere to be only ironic, and too funny to depend on nostalgia. More than twenty years later, it still feels like a creature nobody could reproduce in a laboratory. Thank goodness. Some things should remain unique.</p><p><strong>VERDICT</strong></p><p><strong>9/10 tater tots hidden in a pocket</strong></p><p><em>Gosh!</em></p>]]></content:encoded>
    </item>
    <item>
        <title>How I Built Pablo&#x27;s Room</title>
        <link>https://pablomurad.com/how-i-built-pablos-room/</link>
        <guid isPermaLink="true">https://pablomurad.com/how-i-built-pablos-room/</guid>
        <pubDate>Fri, 17 Jul 2026 22:19:18 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>papers</category>
        <description>A behind-the-scenes look at Pablo’s Room, an interactive pixel-art portfolio built with React, TypeScript, SVG hotspots, a visual editor, serverless storage, accessibility in mind, and one unforgettable cat named Netuno.</description>
        <content:encoded><![CDATA[<p>I wanted a portfolio that felt like a place instead of a page. So I turned my workspace into a pixel-art scene: a cozy isometric office full of monitors, books, gadgets, a window with weather, and my cat, Netuno, napping somewhere in the frame. Every object in the room is clickable. Clicking opens a panel that tells a story — about a project, a tool I use, or just a detail of the room. The whole site behaves like a point-and-click adventure game, but underneath it is a modern, fully typed web application.</p><h1 id="the-stack">The Stack</h1><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>React 19 + Next-style App Router</strong>, built and served through a Vite-based toolchain that compiles the app into a single edge-style worker bundle.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>TypeScript everywhere</strong> — every data structure in the project has an explicit contract.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>Tailwind CSS 4 plus hand-written CSS</strong> for the retro CRT/neon look, with two bundled terminal fonts (VT323 and IBM Plex Mono).</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>Framer Motion</strong> for motion, always gated behind reduced-motion preferences.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>A serverless-style runtime</strong>: the production build runs as a worker on a lightweight local engine, with a SQL database for published content and an object store for uploaded media.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>Docker</strong> for self-hosting, sitting behind my own reverse proxy and HTTPS domain.</p><p>There are no paid services in the loop. The same build can be deployed to an edge platform or run entirely on my own server.</p><h1 id="one-coordinate-system-to-rule-everything">One Coordinate System to Rule Everything</h1><p>The heart of the project is a simple decision: the scene image and the interactive layer share the exact same coordinate space. The room illustration has a fixed logical size, and an SVG overlay uses that same size as its viewBox. Every clickable region — I call them <strong>hotspots</strong> — is defined in those image coordinates. Because the SVG scales with the image, hotspots stay perfectly aligned at any window size, on any device, in any orientation. No math at render time, no drift.</p><p>A hotspot is a small typed record: an id, a slug, a title, a shape (polygon, rectangle, circle or ellipse), its geometry, visibility flags, a category, tags, a z-index, and a content block. The whole room is one versioned JSON document with a schemaVersion field so future migrations stay possible.</p><h1 id="the-content-system">The Content System</h1><p>Each hotspot's content block can carry a short description, long-form Markdown, an image or gallery, video, audio, external links, metadata pairs, references to a small library of fictional books, and even custom commands for a fake in-page terminal. The terminal is a toy — it understands a handful of friendly commands and never touches the real system. Longer texts live as Markdown files, ready to be moved into a CMS later without changing any component.</p><p>Because content is data, not code, redecorating the room never requires touching a component. I edit JSON (or use the visual editor) and the site follows.</p><h1 id="the-public-experience">The Public Experience</h1><p>The public page is composed of a few focused components:</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>RoomExperience</strong> — the orchestrator. It renders the scene, fetches the latest published content from the API at load time (falling back to the bundled document if the network fails), and manages which hotspot is active or open.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>HotspotLayer</strong> — the SVG overlay. It draws each hotspot as a focusable, keyboard-operable shape with an accessible name, hover/focus highlighting, and an on-demand object list for people who prefer picking from a menu instead of hunting with a pointer.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>InfoPanel</strong> — the storytelling surface. It renders the hotspot's Markdown and media, traps focus while open, closes on Escape or outside click, and becomes a bottom drawer on small screens.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>Netuno</strong> — my cat, as a component. He is positioned in the same coordinate system as his hotspot, breathes, flicks his tail, and reacts when you pay attention to him. His animations can be disabled independently of everything else.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <strong>AmbientRoom</strong> — subtle life for the scene itself: glows, flickers and drifting effects anchored to the room's geometry, all disabled automatically when the visitor prefers reduced motion.</p><p>The title banner is a neon terminal prompt: a CRT-style font, a blinking cursor, layered neon glow, and an occasional sign-flicker — all pure CSS, all switched off for reduced-motion users.</p><h1 id="the-visual-editor">The Visual Editor</h1><p>Editing polygon coordinates by hand gets old fast, so the project includes a full visual editor. I can draw polygons vertex by vertex, drag out rectangles, circles and ellipses, move shapes, drag individual vertices, add or remove points, and edit every content field in a side panel. It supports undo/redo history, duplicating hotspots, copying and pasting hotspot JSON, previewing the public panel in place, and hiding, disabling or deleting areas with confirmation. Keyboard shortcuts cover the usual verbs — save, undo, duplicate, nudge by pixel.</p><p>Draft work autosaves locally in the browser. Import and export round-trip the same JSON document the site ships with, and imports are validated structurally before they are accepted — version, image dimensions, unique ids, shape geometry. A backup of the previous state is kept before any import.</p><h1 id="publishing-and-administration">Publishing and Administration</h1><p>The public site is read-only. Writing goes through a small admin area with a login form. Authentication is intentionally boring and robust: credentials are checked server-side with constant-time comparison, and a successful login issues a signed, HTTP-only, strict-same-site session cookie with a short lifetime. The signature uses HMAC with a server-held secret, so sessions cannot be forged or extended from the outside. All secrets and credentials live in environment variables — none of them are in the repository.</p><p>Publishing takes the editor's current document, validates it again on the server (never trust the client, even when the client is me), and stores it in the database as the single published revision. Uploaded media — images, audio, the background-music tape — goes to the object store and is served back through a small asset route with proper content types and range support.</p><figure class="kg-card kg-image-card"><img src="https://pablomurad.com/content/images/2026/07/room1.png" class="kg-image" alt="" loading="lazy" width="1319" height="928" srcset="https://pablomurad.com/content/images/size/w600/2026/07/room1.png 600w, https://pablomurad.com/content/images/size/w1000/2026/07/room1.png 1000w, https://pablomurad.com/content/images/2026/07/room1.png 1319w" sizes="(min-width: 720px) 720px"></figure><h1 id="storage">Storage</h1><p>Published content lives in a tiny SQL schema: one table for the published site document (as JSON, with a timestamp) and one for upload metadata. The media files themselves live in an object store keyed by upload. The schema is created on demand, so a fresh volume boots into a working site with the bundled default content — the database is an overlay on top of the built-in room, never a requirement.</p><h1 id="keeping-it-light">Keeping It Light</h1><p>The production container is deliberately minimal. The application compiles into a self-contained bundle of about ten megabytes, so the runtime image installs only the worker engine — none of the build-time dependencies ship to production. The container starts a single small supervisor process plus the worker engine itself, idles at roughly 130 MB of RAM, and is capped with hard memory and CPU limits plus log rotation. Persistence uses the same on-disk layout as the standard tooling, so data survives upgrades and container rebuilds.</p><h1 id="accessibility">Accessibility</h1><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Every hotspot is a real focusable control with an accessible name, operable by Enter or Space.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; An object list offers a precision-free way to open any item without pointer accuracy.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; The info panel traps focus, restores it on close, and closes by Escape, button, or outside click.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Reduced motion is respected at three levels: the OS preference, a global animation toggle, and a separate toggle just for the cat.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Exploration progress (which objects you have visited) is stored locally and can be reset or turned off.</p><h1 id="quality">Quality</h1><p>The project ships with a unit and component test suite covering the geometry utilities, document validation, editor history, authentication logic and the key components, plus strict TypeScript checking and linting. Production validation is a single ritual: build, test, lint, type-check. I also smoke-test the container itself — routes, login, static assets and storage layout — before calling a change done.</p><h1 id="serving-it-on-my-domain">Serving It on My Domain</h1><p>In production, the container binds only to the loopback interface and my reverse proxy terminates HTTPS for the public domain and forwards requests to it. The app is proxy-friendly by construction: no hardcoded hosts anywhere, relative URLs throughout, secure cookies in production, and absolute social-preview URLs generated from the canonical domain so shared links unfurl correctly.</p><figure class="kg-card kg-image-card"><img src="https://pablomurad.com/content/images/2026/07/room2.png" class="kg-image" alt="" loading="lazy" width="470" height="756"></figure><h1 id="what-i-would-tell-past-me">What I Would Tell Past Me</h1><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Lock the coordinate system first. Every feature after that became easier because geometry was settled.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Make content data from day one. The editor, the API and the CMS-shaped future all fell out of that decision.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Validate at every boundary — imports, API writes, stored documents. Each validator earned its keep.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Treat the runtime as part of the design. Cutting the production container down to the essentials was as satisfying as any visual feature.</p><p>•&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Put the cat in. People remember the cat.</p>]]></content:encoded>
    </item>
    <item>
        <title>The Black Cat</title>
        <link>https://pablomurad.com/the-black-cat/</link>
        <guid isPermaLink="true">https://pablomurad.com/the-black-cat/</guid>
        <pubDate>Tue, 14 Jul 2026 01:58:55 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>papers</category>
        <description>I turned copyparty into a shell-free webtilde: a small community where members edit personal sites securely, publish to /~username/ pages, and build a handmade corner of the web without touching the server.</description>
        <content:encoded><![CDATA[<h2 id="how-i-turned-copyparty-into-a-tilde-without-shell-access">How I Turned copyparty into a Tilde Without Shell Access</h2>
<h3 id="the-idea-the-architecture-and-the-building-of-a-personal-web-community-on-a-server-that-was-already-alive">The idea, the architecture, and the building of a personal-web community on a server that was already alive</h3>
<hr>
<h2 id="first-things-first-asking-the-right-question">First Things First: Asking the Right Question</h2>
<p>The idea began with a small, almost casual question: if copyparty can serve files, deliver an <code>index.html</code>, and apply different permissions per user and per directory, why couldn't I use it as the foundation for a tilde?</p>
<p>At first, the connection seemed slightly crooked. A traditional tilde is a shared Unix machine. A user receives an account, logs in over SSH, gets a home directory, works inside <code>public_html</code>, learns commands, runs programs, talks to other people, and gradually starts inhabiting the server rather than merely publishing through it. Tilde.club describes that tradition as access to a shared Unix computer where people create web pages, learn, and share knowledge.</p>
<p>copyparty, by contrast, presents itself as a portable file server: a browser interface, resumable uploads, WebDAV, SFTP, indexing, accounts, volumes, and directory-level permissions. It would have been easy to look at it and see nothing more than a strange, unusually capable Dropbox.</p>
<p>I saw something else: a publishing panel for the personal web.</p>
<p>The trick was not pretending that copyparty was a multi-user Unix system. It is not. The trick was separating tilde culture from one specific implementation. I wanted to preserve the parts that mattered to me: personal pages, <code>/~username/</code> URLs, a small community, quotas, rules, a member directory, the freedom to edit HTML, and that old feeling that the web can still be made by hand.</p>
<p>I was willing to give up shell access in exchange for a much friendlier front door.</p>
<blockquote>
<h2 id="the-projects-thesis">The Project's Thesis</h2>
<p>A tilde does not have to lose its soul merely because access to <code>public_html</code> happens through a browser. I was not building a complete pubnix. I was building a deliberately limited webtilde: safer, more approachable, and centered on publishing.</p>
</blockquote>
<hr>
<h2 id="what-i-wanted-to-preserve-%E2%80%94-and-what-i-chose-not-to-imitate">What I Wanted to Preserve — and What I Chose Not to Imitate</h2>
<p>Before installing anything, I had to be honest about the idea. If I called any public folder containing HTML a tilde, the word would become decoration. So I defined a minimum cultural core.</p>
<table>
<thead>
<tr>
<th>I wanted to preserve</th>
<th>I would not try to reproduce</th>
</tr>
</thead>
<tbody>
<tr>
<td>Personal pages at <code>/~username/</code></td>
<td>Unix shell access for members</td>
</tr>
<tr>
<td>A small, recognizable community</td>
<td>Persistent user processes</td>
</tr>
<tr>
<td>HTML, CSS, images, text, and small experiments</td>
<td>Arbitrary compilers and runtimes</td>
</tr>
<tr>
<td>Quotas and human approval</td>
<td>Open, automatic registration</td>
</tr>
<tr>
<td>A member directory and public rules</td>
<td>Personal services and custom ports</td>
</tr>
<tr>
<td>Real files outside a CMS</td>
<td>PHP, databases, or WordPress per member</td>
</tr>
</tbody>
</table>
<p>That limitation was not a hidden defect. It was the product.</p>
<p>A member would receive static space, an editor account, a quota, and a URL. They would not receive access to Debian, Docker, Pangolin, Traefik, or any other part of the infrastructure.</p>
<p>That drastically reduced the attack surface and removed much of the thankless work involved in operating an open pubnix: fork bombs, cryptomining, abandoned processes, arbitrary listening ports, compilers, Unix permission puzzles, quota evasion, and CPU abuse.</p>
<hr>
<h2 id="why-copyparty-fit-better-than-it-first-appeared">Why copyparty Fit Better Than It First Appeared</h2>
<p>copyparty had three characteristics that changed the entire discussion.</p>
<p>The first was its volume model: a URL can be mapped to a real directory, and each volume can receive permissions per user.</p>
<p>The second was the web interface. It could upload, rename, move, delete, and edit files without requiring members to understand SSH, SCP, Unix ownership, or command-line editors.</p>
<p>The third was the ability to treat a directory as a traditional website, serving <code>index.html</code> instead of exposing a file listing.</p>
<p>On top of that, copyparty already provided the unglamorous pieces I did not want to rebuild:</p>
<ul>
<li>authentication;</li>
<li>password hashing;</li>
<li>resumable uploads;</li>
<li>per-volume storage limits;</li>
<li>maximum file counts;</li>
<li>maximum individual file sizes;</li>
<li>minimum free-disk reserves;</li>
<li>indexing;</li>
<li>account and volume reloads without bringing down the entire process.</li>
</ul>
<p>The detail that made the project genuinely possible was realizing that the <strong>same files could be presented by two separate copyparty instances</strong>.</p>
<p>One instance would be an authenticated editor with write access.</p>
<p>The other would be an anonymous, read-only public server.</p>
<p>That split looks obvious after the fact. Before it, the project was merely a charming idea. After it, it became architecture.</p>
<hr>
<h2 id="the-server-was-not-a-blank-slate">The Server Was Not a Blank Slate</h2>
<p>I did not begin with an empty VPS. The machine already mattered.</p>
<p>It was a Debian 13 virtual machine running under QEMU and serving as one of the central points in my infrastructure. Before the project, it had roughly 8 GiB of RAM and 150 GB of storage. I upgraded it to around 17 GiB of RAM, 1 TB of disk, and added 4 GiB of swap.</p>
<p>Storage stopped being the problem. The real risk became breaking what was already working.</p>
<p>The server already hosted several components:</p>
<ul>
<li>Pangolin, Gerbil, Traefik, and CrowdSec under Docker Compose;</li>
<li>a Keila installation with PostgreSQL;</li>
<li>an FRP server running directly on the host;</li>
<li>Tailscale, SSH, SNMP, Exim, cron, and the usual Debian services;</li>
<li>ports 80 and 443 already owned by the Gerbil/Traefik path;</li>
<li>an external Docker network named <code>pangolin</code>, used by local services that needed to reach the proxy.</li>
</ul>
<p>That imposed a rule that guided the entire build:</p>
<blockquote>
<p>copyparty would not receive its own public host port.</p>
</blockquote>
<p>There would be no <code>3923:3923</code> mapping followed by crossed fingers. The containers would remain invisible on the host and would be reachable only through the existing proxy on the existing Docker network.</p>
<hr>
<h2 id="the-first-real-step-was-installing-nothing">The First Real Step Was Installing Nothing</h2>
<p>Before creating the project, I audited the server.</p>
<p>I inventoried:</p>
<ul>
<li>CPU;</li>
<li>memory and swap;</li>
<li>disks and filesystems;</li>
<li>mounts;</li>
<li>listening TCP and UDP ports;</li>
<li>systemd services and timers;</li>
<li>local users;</li>
<li>cron jobs;</li>
<li>firewall state;</li>
<li>Docker containers;</li>
<li>Docker networks;</li>
<li>volumes and bind mounts;</li>
<li>Compose projects;</li>
<li>logs;</li>
<li>the directories consuming the most storage.</li>
</ul>
<p>That may sound excessively cautious, but it was the opposite. It was the fastest way to stop guessing.</p>
<p>The audit exposed an important topology. Traefik did not publish ports directly; it shared Gerbil's network namespace. Gerbil was the component actually holding ports 80 and 443.</p>
<p>It also showed that the existing projects relied on bind mounts rather than named Docker volumes, and that <code>/srv</code> was almost empty. That was where I decided to create <code>/srv/theblack-cat</code> as the isolated root of the experiment.</p>
<blockquote>
<h2 id="a-rule-i-kept-afterwards">A Rule I Kept Afterwards</h2>
<p>On a server with history, installing first and understanding later is the wrong order. An inventory is not bureaucracy. It tells you where a new service can exist without competing for ports, networks, storage, or authority with production systems.</p>
</blockquote>
<hr>
<h2 id="the-architecture-that-solved-the-problem">The Architecture That Solved the Problem</h2>
<p>The final design used two persistent copyparty instances, two web origins, and one content tree.</p>
<pre><code class="language-text">Internet
   |
   v
Traefik / HTTPS
   |
   +--&gt; theblack.cat
   |       |
   |       +--&gt; copyparty-public
   |               /w/sites      (read-only)
   |               /w/community  (read-only)
   |
   +--&gt; edit.theblack.cat
           |
           +--&gt; copyparty-editor
                   /w/sites      (read-write)
                   /w/community  (read-write)
</code></pre>
<p>The main domain became the display window. The <code>edit</code> subdomain became the file-management panel.</p>
<p>When I changed <code>/community/index.html</code> through the editor, that same file was already being read by the public instance. There was no deployment step, no publish button, no build pipeline, and no intermediate copy.</p>
<p>Saving was publishing.</p>
<p>At the same time, the public container mounted the shared directories with <code>:ro</code>. Even if an application flaw attempted a write, Docker's mount policy would block it.</p>
<p>That redundancy was intentional:</p>
<ul>
<li>copyparty permissions at the application layer;</li>
<li>read-only mounts at the container/filesystem layer.</li>
</ul>
<hr>
<h2 id="foundation-a-service-identity-and-a-predictable-tree">Foundation: A Service Identity and a Predictable Tree</h2>
<p>I created a system user named <code>theblackcat</code>, with no password and <code>/usr/sbin/nologin</code> as its shell. It did not join the <code>docker</code> group and received no SSH access.</p>
<p>The <code>pablo</code> account would exist inside copyparty. It did not need to become another human Linux account.</p>
<pre><code class="language-text">/srv/theblack-cat/
├── config/
│   ├── editor/
│   └── public/
├── sites/
├── community/
├── members/
├── secrets/
├── state/
├── hists/
├── administration/
├── policies/
├── audit/
├── backup/
├── scripts/
└── release/
</code></pre>
<p>From the start, I treated configuration, content, runtime state, and secrets as different categories.</p>
<ul>
<li>Configuration could be read by containers.</li>
<li>Session state needed to be writable, but isolated per instance.</li>
<li>Member sites were writable only in the editor.</li>
<li>Secrets used mode <code>0600</code>.</li>
<li>The public service never received the authentication file.</li>
</ul>
<p>That separation made later decisions much easier because each path already had a clear purpose and trust level.</p>
<hr>
<h2 id="governance-before-registration">Governance Before Registration</h2>
<p>The system began as a closed alpha.</p>
<p>I deliberately did not start with a signup form, because receiving applications was not the difficult problem. The difficult problem was deciding what happened after someone applied.</p>
<p>So governance came before automation.</p>
<p>I established that every account and every quota required my explicit approval. The administrative account received 10 GiB because it would also be used for testing and for the community website.</p>
<p>Ordinary members could receive one of four approved allocations:</p>
<ul>
<li>100 MiB;</li>
<li>250 MiB;</li>
<li>500 MiB;</li>
<li>1024 MiB.</li>
</ul>
<p>The requested value would never become the approved value automatically.</p>
<p>I also set limits that made sense for personal websites:</p>
<ul>
<li>5,000 files;</li>
<li>50 MiB per individual file;</li>
<li>a global reserve of 100 GiB free on the server.</li>
</ul>
<p>The goal was never to host video libraries. The goal was to prevent a webtilde from accidentally becoming a file locker.</p>
<hr>
<h2 id="the-editor-configuration">The Editor Configuration</h2>
<p>Inside the editor instance, each member receives a dedicated volume.</p>
<p>In my case, <code>/~pablo</code> pointed to <code>/w/sites/pablo</code>. The administrative <code>A</code> permission remained exclusive to me. Ordinary members would receive <code>rwmd</code>, enough to read, upload, move, and delete their own files without receiving application-level administrative power.</p>
<pre><code class="language-ini">[/~example]
  /w/sites/example

  accs:
    rwmd: example

  flags:
    e2ds
    nohtml
    xvol
    vmaxb: 250m
    vmaxn: 5k
    sz: 1-50m
    df: 100g
</code></pre>
<p>The <code>nohtml</code> flag was crucial.</p>
<p>The editor needed to manipulate HTML without executing a member's HTML inside the authenticated origin. With <code>nohtml</code>, HTML and SVG are returned as text, and related rendering behavior is restricted.</p>
<p>That distinction is subtle but important:</p>
<blockquote>
<p>The place where I log in should not behave like the website a member is building.</p>
</blockquote>
<p>The quotas use <code>vmaxb</code> and <code>vmaxn</code>. Because those controls depend on volume indexing, each volume also enables <code>e2ds</code>.</p>
<p>copyparty performs the accounting. I did not need to build another layer just to count files and bytes.</p>
<hr>
<h2 id="the-public-configuration">The Public Configuration</h2>
<p>The public instance is almost the inverse.</p>
<p>It knows nothing about accounts and never receives the authentication secret. Each personal site receives only the <code>h</code> permission, which serves the directory as a traditional website and delivers <code>index.html</code> without turning the domain into a file browser.</p>
<pre><code class="language-ini">[/~example]
  /w/sites/example

  accs:
    h: *

  flags:
    noscript
    xvol
    norobots
    cachectl: no-cache
</code></pre>
<p>During the alpha, I kept <code>noscript</code> enabled. That was not a religious position against JavaScript. It was a way to reduce variables while I validated the model. HTML and CSS were more than enough to prove the concept.</p>
<p>Later, the institutional community website received the root volume <code>/</code>, pointing to <code>/w/community</code>. The <code>/~username</code> volumes remained as child volumes and shadowed the root where appropriate.</p>
<p>The result was simple:</p>
<ul>
<li>the community homepage lived at <code>theblack.cat/</code>;</li>
<li>personal pages remained at <code>theblack.cat/~username/</code>.</li>
</ul>
<hr>
<h2 id="containers-what-i-explicitly-refused-to-give-them">Containers: What I Explicitly Refused to Give Them</h2>
<p>The image was pinned by digest rather than <code>latest</code>.</p>
<p>Both containers run as the UID and GID of the technical service account, with:</p>
<ul>
<li>a read-only root filesystem;</li>
<li>all Linux capabilities dropped;</li>
<li><code>no-new-privileges</code> enabled;</li>
<li>memory limits;</li>
<li>PID limits;</li>
<li>log rotation;</li>
<li>health checks;</li>
<li>no Docker socket;</li>
<li>no privileged mode;</li>
<li>no host networking;</li>
<li>no published host ports.</li>
</ul>
<p>An abbreviated Compose example looks like this:</p>
<pre><code class="language-yaml">services:
  editor:
    image: copyparty/ac@sha256:EXAMPLE_DIGEST
    user: "999:986"
    read_only: true
    restart: unless-stopped
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    pids_limit: 256
    mem_limit: 1g
    volumes:
      - ./sites:/w/sites:rw
      - ./community:/w/community:rw

  public:
    image: copyparty/ac@sha256:EXAMPLE_DIGEST
    user: "999:986"
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    volumes:
      - ./sites:/w/sites:ro
      - ./community:/w/community:ro

networks:
  pangolin:
    external: true
</code></pre>
<p>The external network became the bridge between separate Compose projects.</p>
<p>Services could be resolved by name inside the <code>pangolin</code> network while the host continued to expose no listener on port 3923. That avoided creating a second public entry point or duplicating the proxy stack.</p>
<hr>
<h2 id="the-session-state-problem-that-appeared-only-because-i-did-things-properly">The Session-State Problem That Appeared Only Because I Did Things Properly</h2>
<p>During isolated tests, copyparty warned that it had no safe writable location for session state.</p>
<p><code>/cfg</code> was mounted read-only, and the service account did not have a useful writable home directory. The easy answer would have been enabling an unsafe option.</p>
<p>I chose to fix the cause instead.</p>
<p>I created separate state directories for the editor and public instances, mounted each one at <code>/state</code>, and set:</p>
<pre><code class="language-text">HOME=/state
XDG_CONFIG_HOME=/state/xdg
</code></pre>
<p>After that, the editor created a real session database. The warning disappeared without weakening the container.</p>
<p>This was a useful reminder that hardening is not simply a list of restrictions. A service still needs explicit writable locations for the state it legitimately owns.</p>
<hr>
<h2 id="putting-the-project-behind-the-infrastructure-that-already-existed">Putting the Project Behind the Infrastructure That Already Existed</h2>
<p>The first plan was to register both resources directly in Pangolin.</p>
<p>Discovery revealed an important detail: the available Pangolin sites represented remote tunnels. There was no local site capable of resolving containers on that same VPS.</p>
<p>Forcing the model would have produced a resource that looked elegant in the dashboard and failed in practice.</p>
<p>The solution was to use Traefik's file provider, already the established pattern for other local services on that machine.</p>
<p>I added two routers and two services while preserving:</p>
<ul>
<li>HTTP-to-HTTPS redirection;</li>
<li>security headers;</li>
<li>automatic certificate issuance.</li>
</ul>
<p>A simplified example:</p>
<pre><code class="language-yaml">http:
  routers:
    blackcat-public:
      rule: Host(`theblack.cat`)
      entryPoints:
        - websecure
      service: blackcat-public
      tls:
        certResolver: letsencrypt

    blackcat-editor:
      rule: Host(`edit.theblack.cat`)
      entryPoints:
        - websecure
      service: blackcat-editor
      tls:
        certResolver: letsencrypt

  services:
    blackcat-public:
      loadBalancer:
        servers:
          - url: http://theblackcat-public:3923

    blackcat-editor:
      loadBalancer:
        servers:
          - url: http://theblackcat-editor:3923
</code></pre>
<p>Public traffic reached Traefik, but copyparty remained invisible to the host. TLS terminated at the proxy, and the upstreams were addressed by Docker service name rather than by an exposed port.</p>
<hr>
<h2 id="the-real-ip-trap">The Real-IP Trap</h2>
<p>That was when a bug appeared that exists only when security is partially correct.</p>
<p>Traefik sent <code>X-Forwarded-For</code> containing the visitor's real address, but copyparty did not trust the proxy. Because every request seemed to originate from the same private Docker address, the anti-abuse protection eventually banned the proxy itself.</p>
<p>The result was a <code>403</code> for everyone and the message:</p>
<pre><code class="language-text">thank you for playing
</code></pre>
<p>The investigation showed that Traefik shared Gerbil's network namespace.</p>
<p>Instead of trusting the entire Docker subnet, I allowed only the exact <code>/32</code> of the observed proxy address. For illustration, imagine Gerbil at <code>172.20.0.4</code>:</p>
<pre><code class="language-ini">[global]
  xff-hdr: x-forwarded-for
  xff-src: 172.20.0.4/32
  rproxy: 1
</code></pre>
<p><code>rproxy: 1</code> documented that there was exactly one trusted proxy hop.</p>
<p>After restarting only the two copyparty containers, the proxy-wide ban disappeared. Logs began associating requests with the real visitor IP rather than with the Docker proxy address.</p>
<p>That fix came with an operational warning: the private address was dynamically assigned.</p>
<p>After recreating Gerbil or the Pangolin stack, I would need to verify the address again. The correct answer was not to trust <code>172.16.0.0/12</code> or the entire Docker network. The correct answer was to update the exact <code>/32</code> to the newly observed proxy address.</p>
<hr>
<h2 id="i-did-not-want-to-manage-users-by-editing-files-by-hand">I Did Not Want to Manage Users by Editing Files by Hand</h2>
<p>At that point, the system worked, but it still depended on manual changes to YAML, authentication fragments, and volume configuration files.</p>
<p>That would have been tolerable for me alone and terrible for a community.</p>
<p>The next step was building an administrative CLI: <code>theblackcat-admin</code>.</p>
<p>The tool implemented an explicit lifecycle:</p>
<pre><code class="language-text">pending
  ├── approved
  │     └── active
  │           ├── suspended ──&gt; active
  │           └── archived
  └── rejected
</code></pre>
<p>Each mutating operation uses:</p>
<ul>
<li>a lock;</li>
<li>a backup;</li>
<li>atomic writes;</li>
<li>validation;</li>
<li>rollback;</li>
<li>an audit event written as JSON Lines.</li>
</ul>
<p>The tool never accepts a plaintext password. The administrator generates a hash interactively with copyparty's own utility and installs only the resulting value.</p>
<p>A normal onboarding sequence looks like this:</p>
<pre><code class="language-bash">theblackcat-admin candidate-add alice \
  --display-name "Alice" \
  --email "alice@example.net" \
  --requested-quota 250

theblackcat-admin approve alice \
  --quota 250 \
  --approved-by pablo

theblackcat-admin credential-instructions alice

theblackcat-admin credential-install alice

theblackcat-admin activate alice \
  --approved-by pablo
</code></pre>
<p>Activating a member creates:</p>
<ul>
<li>the member directory;</li>
<li>the initial homepage;</li>
<li>the editor volume;</li>
<li>the public volume;</li>
<li>the authentication entry;</li>
<li>the public directory listing entry.</li>
</ul>
<p>It then reloads accounts and volumes and validates the result.</p>
<p>Suspending a member performs the inverse without deleting anything. Archiving removes publication and authentication while preserving files.</p>
<p>I deliberately did not implement deletion. Removing data should be a separate, reviewed decision, not an accidental side effect of moderation.</p>
<hr>
<h2 id="reloading-without-restarting-the-service">Reloading Without Restarting the Service</h2>
<p>Accounts and volumes are reloaded with <code>USR1</code>:</p>
<pre><code class="language-bash">docker kill --signal=USR1 theblackcat-editor
docker kill --signal=USR1 theblackcat-public
</code></pre>
<p>This mattered because a member could be activated or suspended without recreating containers and without interrupting existing pages.</p>
<p>The CLI records <code>StartedAt</code>, sends the signal, checks container health, and confirms that the process did not restart.</p>
<p>Changes to the global configuration still require a restart, and I treated them as a separate class of operation. The ordinary member lifecycle does not touch global settings.</p>
<hr>
<h2 id="the-community-became-more-than-a-single-pablo">The Community Became More Than a Single <code>/~pablo/</code></h2>
<p>The final public layer was the community website itself.</p>
<p>I created:</p>
<ul>
<li>a homepage;</li>
<li>an About page;</li>
<li>a Members page;</li>
<li>a Rules page;</li>
<li>an Alpha Status page.</li>
</ul>
<p>Everything uses local HTML and CSS, with no JavaScript, remote fonts, analytics, or trackers. <code>robots.txt</code> blocks indexing, and responses also carry <code>noindex</code> instructions.</p>
<p>The member page is not maintained by hand. The CLI generates it from protected member records and exposes only:</p>
<ul>
<li>username;</li>
<li>public display name;</li>
<li>public URL;</li>
<li>a safe role label.</li>
</ul>
<p>Email, quota, internal notes, suspension reasons, and timestamps never enter the public HTML.</p>
<p>Pablo appears as the founding administrator. An ordinary member appears only when their status is exactly <code>active</code>. Suspending or archiving someone regenerates the directory within the same transaction.</p>
<hr>
<h2 id="backup-the-part-that-never-looks-impressive-in-a-screenshot">Backup: The Part That Never Looks Impressive in a Screenshot</h2>
<p>I used Restic to create encrypted, deduplicated, testable snapshots.</p>
<p>The initial repository remained on the same virtual disk, outside the project tree. That provides rollback and recovery from human mistakes, but it is not disaster recovery.</p>
<p>I wrote that limitation in large letters because calling a same-disk copy an offsite backup would be dishonest.</p>
<p>The system received:</p>
<ul>
<li>daily snapshots;</li>
<li>weekly maintenance;</li>
<li>7 daily snapshots retained;</li>
<li>4 weekly snapshots retained;</li>
<li>6 monthly snapshots retained;</li>
<li>1 yearly snapshot retained.</li>
</ul>
<p>Maintenance runs <code>forget</code> with <code>prune</code>, followed by a repository check:</p>
<pre><code class="language-bash">restic forget \
  --keep-daily 7 \
  --keep-weekly 4 \
  --keep-monthly 6 \
  --keep-yearly 1 \
  --prune

restic check --read-data-subset=10%
</code></pre>
<p>I also wrote a real restore test. It restores the latest snapshot into a temporary directory, validates critical files, YAML, JSONL, Compose configuration, and permissions, securely removes restored secret files, and deletes the temporary tree.</p>
<p>The repository password lives outside the project in a root-only file.</p>
<p>Restic's encryption is valuable precisely because the password matters. Losing both the original machine and that password means losing access to the repository. That is why two operational tasks remain important:</p>
<ol>
<li>export the recovery key to a secure offline location;</li>
<li>configure a genuinely independent offsite destination.</li>
</ol>
<hr>
<h2 id="what-the-system-became">What the System Became</h2>
<p>At the end of the ten-part build, I had something that did not exist as an off-the-shelf product: a copyparty-based webtilde with a community homepage, personal pages, an authenticated editor, read-only publication, quotas, moderation, auditing, backup, and a tested restore procedure.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Function</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>theblack.cat</code></td>
<td>Public site, community pages, and <code>/~username/</code></td>
</tr>
<tr>
<td><code>edit.theblack.cat</code></td>
<td>Authenticated file management</td>
</tr>
<tr>
<td><code>copyparty-editor</code></td>
<td>Write access, quotas, <code>nohtml</code>, and account volumes</td>
</tr>
<tr>
<td><code>copyparty-public</code></td>
<td><code>index.html</code>, <code>h: *</code>, <code>noscript</code>, and read-only mounts</td>
</tr>
<tr>
<td>Traefik</td>
<td>HTTPS, redirection, and routing</td>
</tr>
<tr>
<td><code>theblackcat-admin</code></td>
<td>Approval, activation, suspension, quota management, and auditing</td>
</tr>
<tr>
<td>Restic + systemd</td>
<td>Snapshots, retention, integrity checks, and restore tests</td>
</tr>
</tbody>
</table>
<p>The result is not a pubnix.</p>
<p>It does not offer SSH to members, personal cron jobs, compilers, or long-running processes. I do not hide that.</p>
<p>At the same time, calling it simple static hosting would also miss the point. It has a community, an identity, a URL structure, rules, limits, lifecycle management, and a shared publishing experience.</p>
<hr>
<h2 id="where-the-real-cleverness-was">Where the Real Cleverness Was</h2>
<p>The merit of the idea is not hidden in an obscure configuration line.</p>
<p>The clever part was realizing that a file server could occupy the role of a publishing panel, as long as I did not ask one single instance to act as both editor and public server.</p>
<p>Separating those responsibilities solved almost everything:</p>
<ul>
<li>the editor could require authentication and refuse to execute member HTML;</li>
<li>the public service could deliver <code>index.html</code> without knowing any credentials;</li>
<li>the same files appeared on both sides without a deployment stage;</li>
<li>Docker enforced read-only publication in addition to application permissions;</li>
<li>copyparty's volume model naturally became a <code>/~username/</code> directory structure;</li>
<li>native quotas removed the need for a separate accounting system.</li>
</ul>
<p>From that point on, the rest was edge engineering: integrating the proxy, trusting the correct client address, creating an administrative workflow, testing rollback, and documenting limitations honestly.</p>
<p>The idea opened the door. Rigor kept it from becoming a fragile toy.</p>
<hr>
<h2 id="the-decisions-i-do-not-romanticize">The Decisions I Do Not Romanticize</h2>
<p>Some choices remain compromises.</p>
<p>The local backup is on the same disk and still needs an offsite copy.</p>
<p>The administrative credential used during the build was exposed in conversation and must be rotated before opening invitations to outsiders.</p>
<p>The trusted private proxy address is dynamically assigned and must be revalidated after the stack is recreated.</p>
<p>Those pending items do not invalidate the project. They define its real state.</p>
<p>The Black Cat became operational in owner-only mode. I could edit, publish, test, and administer it. I still should not invite third parties until the credential is rotated.</p>
<p>That operational honesty is worth more than an artificial green badge.</p>
<hr>
<h2 id="what-i-learned-while-building-it">What I Learned While Building It</h2>
<ol>
<li>The best architecture appeared when I stopped trying to make one instance perform contradictory roles.</li>
<li>On existing infrastructure, the initial audit saves more time than any automatic installer.</li>
<li>Read-only at the application layer is not the same as read-only at the container layer; using both is better.</li>
<li>Quotas without governance merely limit bytes. Human approval limits purpose.</li>
<li>A badly configured proxy can turn anti-abuse protection into global downtime.</li>
<li>Administrative automation needs transactions, locks, validation, and rollback even when the project is small.</li>
<li>A backup deserves trust only after a restore has been tested.</li>
<li>A tilde is as much a culture of publishing as it is a Unix machine. Part of that culture can survive without pretending shell access exists.</li>
</ol>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>I began by asking whether copyparty could host simple websites.</p>
<p>I ended with a closed community for personal pages, built on top of a server that already did many important things and could not be treated as a disposable laboratory.</p>
<p>The most satisfying part was seeing that the idea remained simple even after it gained serious security and operational layers.</p>
<p>For a member, the experience is straightforward:</p>
<ol>
<li>log in to the editor;</li>
<li>open their directory;</li>
<li>change <code>index.html</code>;</li>
<li>see the page at <code>/~username/</code>.</li>
</ol>
<p>Underneath that simple experience are separate containers, permissions, quotas, a reverse proxy, TLS, real-client-IP handling, auditing, backups, and restore tests.</p>
<p>The complexity ended up on the correct side of the door.</p>
<p>The Black Cat does not try to replace a traditional Unix tilde. It is another interpretation: a webtilde for people who want to build their own corner of the internet without beginning with SSH.</p>
<p>It was a genuinely good idea because it used exactly what copyparty does best — files, accounts, and volumes — to create something the project never explicitly promised to be, but for which it was surprisingly well prepared.</p>
]]></content:encoded>
    </item>
    <item>
        <title>How I built NyxPE</title>
        <link>https://pablomurad.com/how-i-built-nyxpe/</link>
        <guid isPermaLink="true">https://pablomurad.com/how-i-built-nyxpe/</guid>
        <pubDate>Sun, 12 Jul 2026 11:08:25 -0300</pubDate>
        <dc:creator>Pablo Murad</dc:creator>
        <category>tech</category>
        <description>NyxPE is my custom WinPE rescue environment, built with PhoenixPE and PEBakery. This article covers its design, toolset, offline package system, stubborn bugs, and the work behind turning a personal idea into a bootable release.</description>
        <content:encoded><![CDATA[<p>I do a lot of tech work. Reinstalls, data recovery, dead boot loaders, the usual. And for years I did all of it from other people's rescue disks, mostly Sergei Strelec's WinPE. Nothing wrong with it, honestly, it's a fantastic piece of work. But every single boot I'd be re-fixing the keyboard layout, hunting for a tool three submenus deep, staring at branding that wasn't mine. Small itch, never went away. So I finally sat down and built the thing I actually wanted, and that's NyxPE. Nyx Rescue Environment.</p><p>The base was an easy call: PhoenixPE plus PEBakery. PhoenixPE gives you a clean, scriptable WinPE project, and PEBakery is the engine that turns a pile of scripts into a bootable image. The one rule I set for myself on day one was to never touch the official PhoenixPE scripts. Everything I add lives in my own folder. That way, if I ever want to pull a newer PhoenixPE, I just drop it in and my stuff still sits on top. A little discipline now, a lot of sanity later.</p><h2 id="making-it-feel-like-mine">Making it feel like mine</h2><p>First thing was identity, because if it doesn't look like NyxPE it's just PhoenixPE with extra steps. Dark wallpaper, a moon logo, the "NYX PE" wordmark, a matching splash while it boots. OEM info so the System Properties page says NyxPE instead of the default. And the computer name, which turned out to be weirdly annoying. WinPE loves to call the machine something like PHOENIXPE-VF23 with a random suffix, and after some digging I found the culprit was PENetwork, generating the name from a profile template. So I nailed it in two places: offline in the registry hives during the build, and then again at runtime with a tiny script that fires after the network comes up, because the network stack will happily rename the box out from under you if you let it.</p><p>PENetwork itself needed to shut up. Out of the box it throws a window in your face on boot, runs a countdown, asks questions. I just want ethernet up on DHCP and the icon sitting quietly in the tray for later. So I went through the actual INI keys the bundled version uses (I refused to copy-paste settings off some 2011 forum post and hope for the best), and now it starts silent, grabs an address, and gets out of the way.</p><p>Little things after that. Firefox with exactly one bookmark, my GitHub, nothing else. Killing the Windows boot logo so you get a clean black screen instead of the spinning dots. I did that one with <code>bootuxdisabled</code> in the BCD, no patching of signed files, nothing sketchy with Secure Boot.</p><h2 id="the-part-that-ate-my-weekend">The part that ate my weekend</h2><p>Then I went big. I wanted a real toolbox: backup, imaging, partitioning, file recovery, read-only forensics, malware scanners, network tools, hardware diagnostics. Dozens of programs.</p><p>The catch is the build has to stay offline. I didn't want scripts phoning home in the middle of a build. So I wrapped a little package system around it. A manifest of every app with its official source, a downloader that pulls each one straight from the vendor or the GitHub release, checks the SHA-256 and the Authenticode signature, and stages it locally. Then the PEBakery build just copies from the local stash. If a tool goes missing, the build warns and keeps going instead of blowing up.</p><p>The audit had a nice surprise: PhoenixPE already ships scripts for a ton of apps. So the smart move wasn't to reinvent those, it was to keep the good official ones and only write my own for what was missing. It also forced a decision I feel pretty strongly about. PhoenixPE bundles a whole family of credential-dumping tools, browser password grabbers, LSA secrets, all of that. I pulled every single one out of my profiles. A rescue disk is for fixing your own machines, not lifting passwords off them. The only account tool I kept is a proper local admin reset that warns you, backs up the registry hives first, and never runs silently.</p><h2 id="bugs-so-many-bugs">Bugs. So many bugs.</h2><p>This is where it got real. A few that stuck with me.</p><p>The one that made me feel dumb: my downloader kept staging exactly one app and marking every other one "no resolver defined". Turns out PowerShell variable names are case-insensitive, so <code>$res</code> (my per-app result) and <code>$RES</code> (my big lookup table) were literally the same variable. The first loop clobbered the table and the rest just found nothing. Renamed it, fixed instantly, said a few words out loud.</p><p>Then there was a helper function I named <code>H</code>. PowerShell has a built-in alias <code>h</code> for Get-History, and aliases win over functions, so every call to my <code>H</code> was quietly running Get-History and choking on a category name it was never meant to see. Renamed that too.</p><p>The nasty one: every app installed fine, and every single shortcut was dead. "The item has been changed or moved." Took me a while to trace. I was using DirCopy with a wildcard to drop each program into place, and it turns out DirCopy only grabs the subfolders, not the files sitting in the root. So the <code>mappings</code> folder made it onto the media and the actual <code>.exe</code> never did. Every shortcut pointed at a file that wasn't there. Swapped in FileCopy and DirCopy together and suddenly everything opened.</p><p>And the one that nearly ended me. The build would finish "successfully" and produce no ISO. The imaging tool was dying instantly. When I finally ran it by hand it told me the truth: one of the Eric Zimmerman forensic tools has map files with names over 120 characters, and oscdimg in the default ISO9660 mode caps paths around 110. GSmartControl's GTK icons blew past it too. I renamed files for a while, playing whack-a-mole, before doing the real fix, which was to switch the image to pure UDF. No path limit, and it's what every modern WinPE build uses anyway. Booted BIOS and UEFI on the first try after that.</p><p>Smaller ones too. A .NET tool with no version resource killed the whole build, because I was reading its file version for a marker file. Wrapped it so a missing version is just "n/a" instead of a hard stop.</p><p>Once it actually worked, I started caring about the feel. Command-line tools were the worst. You click one in the Start Menu and a console flashes for a tenth of a second and vanishes, because it's waiting for arguments. So now the CLI tools open a prompt already parked in their own folder, wearing the tool's own icon, ready to type. GUI apps open normally. Sysinternals gets split the same way, since half of it is windows and half is command line.</p><p>I also dropped a usage guide right on the desktop. An HTML page that lists every tool, tells you whether it's a window or a terminal thing, and gives a quick example for the command-line ones. Because future me, at 3am with a dead laptop, does not remember the exact chainsaw syntax.</p><p>All of the testing has been in VMware, watching the splash come up, the desktop load, the network grab an address, the tray behave itself. Seeing NYXPE on that wallpaper for the first time, after all the fighting, was genuinely a good moment.</p><p>It lives at nyxpe.xyz now. The ISO, the guide, a checksum, and a short honest note about licensing, because it's built on Windows PE and a mix of third-party tools, and I'd rather be upfront that this is a personal, for-study project than pretend it's something it isn't. Next I want to swap in a real graphic boot splash and start wiring up separate rescue ISOs for iVentoy, but that's a problem for another weekend.</p>]]></content:encoded>
    </item>
</channel>
</rss>
