<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
    <title>Nows - security</title>
    <subtitle>Nows builds the systems behind fundraising: donation forms, payment integrations, reconciliation tools and the reports that follow.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://nows-portfolio.pages.dev/tags/security/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://nows-portfolio.pages.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-10-23T00:00:00+00:00</updated>
    <id>https://nows-portfolio.pages.dev/tags/security/atom.xml</id>
    <entry xml:lang="en">
        <title>How this site is built</title>
        <published>2026-10-23T00:00:00+00:00</published>
        <updated>2026-10-23T00:00:00+00:00</updated>
        
        <author>
          <name>Unknown</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://nows-portfolio.pages.dev/articles/how-this-site-is-built/"/>
        <id>https://nows-portfolio.pages.dev/articles/how-this-site-is-built/</id>
        
        <content type="html" xml:base="https://nows-portfolio.pages.dev/articles/how-this-site-is-built/">&lt;p&gt;This site has three jobs: show my work, hold my writing, and teach me Rust while I build it. It also had to cost nothing to run. Here is how the pieces fit, and why each one is there.&lt;/p&gt;
&lt;h2 id=&quot;a-static-site-first&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#a-static-site-first&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
A static site first&lt;/h2&gt;
&lt;p&gt;Every page you read here is a plain HTML file, built ahead of time by &lt;a rel=&quot;noreferrer external&quot; href=&quot;https://www.getzola.org/&quot;&gt;Zola&lt;/a&gt;. There is no server rendering pages on request, which means there is nothing to slow down under load, nothing to patch at night, and very little to attack. Cloudflare Pages hosts the files for free and rebuilds them whenever the repository changes.&lt;/p&gt;
&lt;p&gt;Zola is a single program written in Rust. It turns Markdown files into pages, compiles the stylesheet, highlights code and builds the search index you can open with the &lt;kbd&gt;/&lt;/kbd&gt; key.&lt;/p&gt;
&lt;h2 id=&quot;writing-without-a-code-editor&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#writing-without-a-code-editor&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
Writing without a code editor&lt;/h2&gt;
&lt;p&gt;An article is a Markdown file in the repository. That’s great for keeping history, and terrible if every post means opening a terminal. So the site has an editor, built in two stages.&lt;/p&gt;
&lt;h3 id=&quot;stage-one-decap-cms-and-a-rust-sign-in-helper&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#stage-one-decap-cms-and-a-rust-sign-in-helper&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
Stage one: Decap CMS and a Rust sign-in helper&lt;/h3&gt;
&lt;p&gt;&lt;a rel=&quot;noreferrer external&quot; href=&quot;https://decapcms.org/&quot;&gt;Decap CMS&lt;/a&gt; is an editor that runs in the browser at &lt;code&gt;/admin&lt;/code&gt; and saves articles straight into GitHub. It needs one small server: GitHub won’t hand an access token to a web page without a secret that can’t live in a browser. I wrote that server as a Cloudflare Worker in Rust.&lt;/p&gt;
&lt;p&gt;The sign-in helper does three things in order. It sets a random &lt;code&gt;state&lt;/code&gt; value in a cookie and sends you to GitHub. When GitHub sends you back, it checks that the &lt;code&gt;state&lt;/code&gt; in the address matches the cookie. Then it gives the token to the editor window, but only if that window is on my site.&lt;/p&gt;
&lt;p&gt;That last check is the one many helpers skip. Here is the comparison that guards the &lt;code&gt;state&lt;/code&gt; value, written so the time it takes doesn’t reveal how many characters matched:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot; &gt;&lt;code data-lang=&quot;rust&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-15&quot;&gt;pub fn&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt; constant_time_eq&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;: &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; b&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;: &amp;amp;&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;) -&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; bool&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-24&quot;&gt;    let&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt; (&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; b&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;) = (&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;a&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;as_bytes&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;(),&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; b&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;as_bytes&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;());&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-24&quot;&gt;    if&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; a&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt; !=&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; b&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;() {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-24&quot;&gt;        return&lt;/span&gt;&lt;span class=&quot;z-33&quot;&gt; false&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-15&quot;&gt;    }&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-25&quot;&gt;    a&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;iter&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;b&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;z-13&quot;&gt;fold&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;z-33&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;u8&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;acc&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;, (&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; y&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; acc&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt; |&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt; (&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt;x&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt; ^&lt;/span&gt;&lt;span class=&quot;z-25&quot;&gt; y&lt;/span&gt;&lt;span class=&quot;z-15&quot;&gt;))&lt;/span&gt;&lt;span class=&quot;z-24&quot;&gt; ==&lt;/span&gt;&lt;span class=&quot;z-33&quot;&gt; 0&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-15&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&quot;stage-two-my-own-cms-in-rust&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#stage-two-my-own-cms-in-rust&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
Stage two: my own CMS in Rust&lt;/h3&gt;
&lt;p&gt;The second stage replaces Decap with a CMS I wrote. It’s another Rust worker, with drafts stored in Cloudflare D1, a SQLite database that runs next to the worker.&lt;/p&gt;
&lt;p&gt;Publishing doesn’t put the article in the database for the site to read. Instead, the worker writes a Markdown file into the repository through the GitHub API, exactly the kind of file Decap writes, and the site rebuilds. Git stays the single record of what is public. If I ever drop the CMS, every article is still a readable file.&lt;/p&gt;
&lt;blockquote class=&quot;markdown-alert-note&quot;&gt;
&lt;p&gt;The free D1 plan allows 5 million row reads and 100,000 row writes a day. An editor used by one person stays far below that, and the public site never touches the database at all.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;the-rules-live-in-one-tested-library&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#the-rules-live-in-one-tested-library&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
The rules live in one tested library&lt;/h2&gt;
&lt;p&gt;Both workers and two command-line tools share a small Rust library. It does the fiddly, security-sensitive work: checking article fields, making slugs, writing front matter, signing session cookies and rendering a safe preview. It never touches the network or the clock, so all of it runs under &lt;code&gt;cargo test&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;One test I’m fond of feeds the front matter writer a title full of quotes and a description containing &lt;code&gt;+++&lt;/code&gt;, then checks that nothing escapes its field. Because the file is written with a TOML library instead of string formatting, a cleverly worded description can’t switch a draft to published.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Concern&lt;/th&gt;&lt;th&gt;How it’s handled&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Someone else’s page asks for a token&lt;/td&gt;&lt;td&gt;Sign-in hands it only to the configured site address&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Forged session cookie&lt;/td&gt;&lt;td&gt;Cookie is signed with HMAC-SHA256 and expires after 8 hours&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cross-site form posts&lt;/td&gt;&lt;td&gt;&lt;code&gt;SameSite=Strict&lt;/code&gt; cookie, a required custom header, and an origin check&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Script in an article preview&lt;/td&gt;&lt;td&gt;Raw HTML is shown as text; &lt;code&gt;javascript:&lt;/code&gt; links are removed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Script injected into the site&lt;/td&gt;&lt;td&gt;A Content Security Policy that only allows the site’s own files&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;a-factory-of-agents&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#a-factory-of-agents&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
A factory of agents&lt;/h2&gt;
&lt;p&gt;I build with Claude Code, and I’ve set it up like a small production line. Each change becomes a work order that passes through stations: a planner writes the plan, a builder makes the change, a design reviewer checks it against the design system, a security checker looks for problems, and an approver checks the evidence and signs off. Any station can stop the line. A local dashboard shows every order and where it is.&lt;/p&gt;
&lt;p&gt;It’s more ceremony than a personal site needs, and that’s the point. It is practice for working with agents on systems where mistakes cost money.&lt;/p&gt;
&lt;h2 id=&quot;what-it-costs&quot;&gt;&lt;a class=&quot;heading-anchor&quot; href=&quot;#what-it-costs&quot; aria-label=&quot;Link to this section&quot;&gt;#&lt;/a&gt;
What it costs&lt;/h2&gt;
&lt;p&gt;Nothing, as long as I use the free addresses Cloudflare gives me. A custom domain is the only thing I’d pay for, and the site works without one.&lt;/p&gt;
</content>
        
    </entry>
</feed>
