<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Heiswayi Nrird</title>
    <description>Heiswayi Nrird - software architect and engineer building systems with, and around, AI.</description>
    <link>/</link>
    <atom:link href="/feed.xml" rel="self" type="application/rss+xml"/>
    <pubDate>Fri, 11 Sep 2026 13:01:52 GMT</pubDate>
    <lastBuildDate>Fri, 11 Sep 2026 13:01:52 GMT</lastBuildDate>
    <generator>Eleventy</generator>
      <item>
        <title>archmap</title>
        <description>&lt;h2&gt;I wanted a map before reading the code&lt;/h2&gt;
&lt;p&gt;When I open an unfamiliar project, I do not want to start with every file equally important.&lt;/p&gt;
&lt;p&gt;I want the first map.&lt;/p&gt;
&lt;p&gt;Where are the entry points? Where are the models? Which files are only configuration? Which modules depend on which other modules? Is this a small tool with a clean shape, or a folder full of hidden complexity?&lt;/p&gt;
&lt;p&gt;You can discover all of that manually, but it takes time. You open the tree, jump between imports, read filenames, follow conventions, and slowly build a mental model in your head.&lt;/p&gt;
&lt;p&gt;That mental model is useful, but it is also temporary. It stays with the person who did the reading. It gets stale when the project changes. And it is rarely written down unless somebody has the patience to draw a diagram.&lt;/p&gt;
&lt;p&gt;So I built &lt;a href=&quot;https://github.com/heiswayi/archmap&quot;&gt;&lt;code&gt;archmap&lt;/code&gt;&lt;/a&gt;, a new open source project for briefly reviewing any app architecture and making the result presentable as a single self-contained HTML file.&lt;/p&gt;
&lt;h2&gt;What it does&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;archmap&lt;/code&gt; scans a project folder, builds a quick architecture overview, and generates one standalone interactive HTML file.&lt;/p&gt;
&lt;p&gt;No server.&lt;/p&gt;
&lt;p&gt;No database.&lt;/p&gt;
&lt;p&gt;No external assets.&lt;/p&gt;
&lt;p&gt;No runtime dependencies.&lt;/p&gt;
&lt;p&gt;Just run:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;archmap /path/to/project&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then open the generated &lt;code&gt;*-architecture.html&lt;/code&gt; file in a browser.&lt;/p&gt;
&lt;p&gt;The report gives you a few useful views:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;an overview of files, lines, languages, classes, functions, and dependency edges&lt;/li&gt;
&lt;li&gt;inferred architecture layers such as entry points, config, models, API, services, data access, integrations, UI, utilities, tests, docs, and build/ops&lt;/li&gt;
&lt;li&gt;internal dependency links between project files&lt;/li&gt;
&lt;li&gt;a collapsible file tree&lt;/li&gt;
&lt;li&gt;a searchable component reference&lt;/li&gt;
&lt;li&gt;a side panel for each file with its imports, classes, functions, methods, and links to related files&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important part is that everything is embedded in a single HTML file. The project data is serialized as JSON inside the page, so the report is easy to share, archive, or attach to documentation.&lt;/p&gt;
&lt;h2&gt;The architecture is deliberately boring&lt;/h2&gt;
&lt;p&gt;The tool itself is pure Python.&lt;/p&gt;
&lt;p&gt;That choice matters. I wanted it to be easy to install, easy to run, and easy to package as a standalone binary later. A tool like this should not ask for much before it gives you value.&lt;/p&gt;
&lt;p&gt;The pipeline is simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;scan the project tree&lt;/li&gt;
&lt;li&gt;analyze each source file&lt;/li&gt;
&lt;li&gt;classify files into architecture layers&lt;/li&gt;
&lt;li&gt;resolve internal dependencies&lt;/li&gt;
&lt;li&gt;render the result into HTML&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The scanner skips the usual noise: &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;node_modules&lt;/code&gt;, build folders, virtual environments, caches, binary assets, lockfiles, and files larger than about 2 MB. It also honors &lt;code&gt;.gitignore&lt;/code&gt; by default.&lt;/p&gt;
&lt;p&gt;Python files get deeper analysis through the standard library &lt;code&gt;ast&lt;/code&gt; module. That means classes, functions, methods, imports, and module docstrings can be extracted more reliably.&lt;/p&gt;
&lt;p&gt;Other languages use lightweight heuristics. That is not perfect, but it is practical. The goal is not to become a compiler for every language. The goal is to produce a useful architecture map quickly.&lt;/p&gt;
&lt;h2&gt;Layer detection is heuristic, but useful&lt;/h2&gt;
&lt;p&gt;Architecture is often visible in names.&lt;/p&gt;
&lt;p&gt;A file under &lt;code&gt;src/api/routes.js&lt;/code&gt; probably belongs to an API layer. A file under &lt;code&gt;models/&lt;/code&gt; is probably a data model. A &lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;pyproject.toml&lt;/code&gt;, or GitHub Actions workflow belongs closer to build and operations. A file named &lt;code&gt;cli.py&lt;/code&gt; or &lt;code&gt;main.go&lt;/code&gt; is likely an entry point.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;archmap&lt;/code&gt; uses those signals to classify files into layers.&lt;/p&gt;
&lt;p&gt;It looks at directory names, filename hints, and extensions. Directory names carry the strongest signal. Filename hints come next. Extensions are a weaker fallback.&lt;/p&gt;
&lt;p&gt;This is not a formal architecture rule system. It is a first-pass classifier.&lt;/p&gt;
&lt;p&gt;That is enough for the intended use case. When you are trying to understand a project quickly, a good approximation is much better than a blank page.&lt;/p&gt;
&lt;h2&gt;Dependency links make the map more useful&lt;/h2&gt;
&lt;p&gt;A file tree tells you where things live.&lt;/p&gt;
&lt;p&gt;A dependency graph tells you how things relate.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;archmap&lt;/code&gt; tries to resolve imports between files in the same project. For Python, it handles dotted imports and relative imports. For JavaScript and similar languages, it handles path-style imports where possible.&lt;/p&gt;
&lt;p&gt;The report then shows both directions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;what this file depends on&lt;/li&gt;
&lt;li&gt;what uses this file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That second direction is often the more useful one. When you are thinking about changing a file, you usually want to know who will be affected.&lt;/p&gt;
&lt;h2&gt;It is not trying to replace deeper tools&lt;/h2&gt;
&lt;p&gt;There are limits.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;archmap&lt;/code&gt; is not a full static analyzer. It does not understand every framework. It does not evaluate dynamic imports. It does not know your domain architecture. It cannot tell whether your layering is correct.&lt;/p&gt;
&lt;p&gt;And for non-Python languages, the extraction is intentionally heuristic.&lt;/p&gt;
&lt;p&gt;That is fine.&lt;/p&gt;
&lt;p&gt;I see this as a fast orientation tool, not a formal verification tool. It gives you enough structure to start reading with better context.&lt;/p&gt;
&lt;p&gt;The difference is similar to looking at a city map before walking through the streets. The map will not show every detail, but it prevents you from wandering blindly.&lt;/p&gt;
&lt;h2&gt;Why a single HTML file&lt;/h2&gt;
&lt;p&gt;I like tools that leave behind simple artifacts.&lt;/p&gt;
&lt;p&gt;A standalone HTML file is boring in the best way. You can open it locally. You can send it to someone. You can keep it next to release notes. You can regenerate it whenever the project changes.&lt;/p&gt;
&lt;p&gt;There is no service to run and no account to create.&lt;/p&gt;
&lt;p&gt;That also makes it useful in environments where source code cannot leave the machine. You can generate the report locally and keep it local.&lt;/p&gt;
&lt;h2&gt;Installation&lt;/h2&gt;
&lt;p&gt;The package is published as &lt;code&gt;archmap-cli&lt;/code&gt; and provides the &lt;code&gt;archmap&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;pipx&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pipx &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; archmap-cli&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Or with &lt;code&gt;pip&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;pip &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; archmap-cli&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;archmap &lt;span class=&quot;token builtin class-name&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A few useful options:&lt;/p&gt;
&lt;pre class=&quot;language-bash&quot;&gt;&lt;code class=&quot;language-bash&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;# scan a specific project&lt;/span&gt;
archmap /path/to/project

&lt;span class=&quot;token comment&quot;&gt;# write to a custom output file&lt;/span&gt;
archmap /path/to/project &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; architecture.html

&lt;span class=&quot;token comment&quot;&gt;# set the display name&lt;/span&gt;
archmap /path/to/project &lt;span class=&quot;token parameter variable&quot;&gt;--name&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;My Service&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# open the generated report after building it&lt;/span&gt;
archmap /path/to/project &lt;span class=&quot;token parameter variable&quot;&gt;--open&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# ignore .gitignore rules&lt;/span&gt;
archmap /path/to/project --no-gitignore&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;What I like about this kind of tool&lt;/h2&gt;
&lt;p&gt;It does not ask you to change your workflow.&lt;/p&gt;
&lt;p&gt;It does not require you to annotate your code.&lt;/p&gt;
&lt;p&gt;It does not need a configuration file before it can do something useful.&lt;/p&gt;
&lt;p&gt;You point it at a folder and it gives you a map.&lt;/p&gt;
&lt;p&gt;That is enough for many moments: onboarding, refactoring, reviewing a small service, auditing an old project, or handing work to another developer.&lt;/p&gt;
&lt;p&gt;A map does not have to be perfect to be helpful.&lt;/p&gt;
&lt;p&gt;It only has to make the next step clearer.&lt;/p&gt;
&lt;h2&gt;Closing thought&lt;/h2&gt;
&lt;p&gt;Most developers eventually build a private map of every project they touch.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;archmap&lt;/code&gt; turns part of that private map into a real artifact.&lt;/p&gt;
&lt;p&gt;Not the final truth of the codebase. Not complete documentation. Not architecture governance.&lt;/p&gt;
&lt;p&gt;Just a useful first look.&lt;/p&gt;
&lt;p&gt;And sometimes that is exactly what you need before making good decisions.&lt;/p&gt;
</description>
        <pubDate>Wed, 17 Jun 2026 24:00:00 GMT</pubDate>
        <link>/blog/archmap/</link>
        <guid isPermaLink="true">/blog/archmap/</guid>
        <category>archmap</category>
        <category>python</category>
        <category>developer-tools</category>
        <category>static-analysis</category>
        <category>architecture</category>
      </item>
      <item>
        <title>Building stdnotes.app</title>
        <description>&lt;p&gt;I wanted a note-taking app that did one thing well: let me write immediately, without forcing me to create an account, pick a pricing plan, or wait for a server to respond. Everything else - cloud sync, sharing, billing - would be optional layered on top.&lt;/p&gt;
&lt;p&gt;This post covers the architectural decisions behind stdnotes: why I chose each piece of the stack, how the offline-first design works at the implementation level, and what I&apos;d do differently next time.&lt;/p&gt;
&lt;h2&gt;The Problem I Was Solving&lt;/h2&gt;
&lt;p&gt;Most note apps are cloud-first. Your notes don&apos;t exist until they&apos;re on a server. If you&apos;re on a plane, in a basement, or behind a corporate proxy, you&apos;re blocked. And every one of them requires an account before you can type a single character.&lt;/p&gt;
&lt;p&gt;I wanted to flip this: &lt;strong&gt;start offline by default, opt into the cloud later&lt;/strong&gt;. The mental model is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open the app → write immediately&lt;/li&gt;
&lt;li&gt;Notes live in IndexedDB (your browser&apos;s built-in database)&lt;/li&gt;
&lt;li&gt;Sign in with Google whenever you want → notes sync to Firestore&lt;/li&gt;
&lt;li&gt;Never signed in? Your notes are still there, every time you open the app&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This sounds obvious, but it has real architectural consequences that ripple through the entire codebase.&lt;/p&gt;
&lt;h2&gt;Architecture Overview&lt;/h2&gt;
&lt;pre class=&quot;language-mermaid&quot;&gt;&lt;code class=&quot;language-mermaid&quot;&gt;flowchart LR
    user((User))

    frontend[&quot;Frontend&amp;lt;br/&gt;Next.js&quot;]
    indexeddb[(&quot;IndexedDB&amp;lt;br/&gt;Offline Store&quot;)]
    backend[&quot;Backend API&amp;lt;br/&gt;Fastify&quot;]
    firestore[(&quot;Firestore&amp;lt;br/&gt;Cloud Database&quot;)]
    auth[&quot;Firebase Auth&amp;lt;br/&gt;Authentication Service&quot;]

    user --&gt; frontend

    frontend &amp;lt;--&gt;|HTTP / REST| backend
    frontend &amp;lt;--&gt;|WebSocket&amp;lt;br/&gt;Live Sync| backend

    frontend &amp;lt;--&gt;|Read / Write&amp;lt;br/&gt;Offline Cache| indexeddb

    backend &amp;lt;--&gt;|Read / Write| firestore
    frontend --&gt;|Sign in / Session| auth
    backend --&gt;|Verify Auth Token| auth

    classDef app fill:#eef6ff,stroke:#2563eb,stroke-width:1.5px,color:#111827;
    classDef db fill:#f0fdf4,stroke:#16a34a,stroke-width:1.5px,color:#111827;
    classDef service fill:#fff7ed,stroke:#ea580c,stroke-width:1.5px,color:#111827;
    classDef user fill:#f5f3ff,stroke:#7c3aed,stroke-width:1.5px,color:#111827;

    class frontend,backend app;
    class indexeddb,firestore db;
    class auth service;
    class user user;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stack is a TypeScript monorepo with three workspaces:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stdnotes/
├── frontend/   → Next.js 15 + React 19
├── backend/    → Fastify 5 + firebase-admin
└── packages/shared/  → shared TypeScript types, constants
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The key architectural constraint: &lt;strong&gt;the frontend never talks to Firestore directly&lt;/strong&gt;. There is no Firestore client SDK in the browser. All database operations go through the Fastify backend, which uses the Firebase Admin SDK. This keeps auth enforcement in one place, prevents client-side rule bypasses, and makes the frontend portable to any database backend.&lt;/p&gt;
&lt;h2&gt;The Dual Storage Backend&lt;/h2&gt;
&lt;p&gt;The offline-first requirement forced me to design a clean abstraction for storage operations early on. I ended up with a &lt;code&gt;Backend&lt;/code&gt; interface that both the online and offline implementations satisfy:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Backend&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;createNote&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;note&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Partial&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Note&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Note&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;updateNote&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; patch&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Partial&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;Note&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;deleteNote&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;id&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;Promise&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token function&quot;&gt;subscribeNotes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;cb&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;notes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; NoteMeta&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Unsubscribe&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// ... folders, settings, etc.&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Offline backend&lt;/strong&gt; (&lt;code&gt;createOfflineBackend&lt;/code&gt;): reads and writes to IndexedDB via the &lt;code&gt;idb&lt;/code&gt; library. Subscriptions use a local pub-sub emitter - when you create a note, subscribers are notified synchronously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Online backend&lt;/strong&gt; (&lt;code&gt;createOnlineBackend&lt;/code&gt;): sends HTTP requests to the Fastify API and subscribes to real-time updates via WebSocket streams.&lt;/p&gt;
&lt;p&gt;The workspace context picks which implementation to use based on auth state. React components never know which backend is active - they just call &lt;code&gt;backend.createNote()&lt;/code&gt; and it works in both modes.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// Inside workspace-context.tsx&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; backend &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; user
  &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createOnlineBackend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;apiClient&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; authToken&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;createOfflineBackend&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;idbStore&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This design means switching from offline to online mode is a context update, not a data migration. The note list re-renders with cloud data; local IndexedDB data is preserved for manual migration later.&lt;/p&gt;
&lt;h2&gt;Content Compression with lz-string&lt;/h2&gt;
&lt;p&gt;Storing thousands of notes in IndexedDB (and later in Firestore) made me think about size. A rich-text note with some formatting can easily be 5–15 KB of HTML. Over 1,000 notes, that&apos;s 5–15 MB in the browser&apos;s storage quota.&lt;/p&gt;
&lt;p&gt;I chose &lt;a href=&quot;https://github.com/pieroxy/lz-string&quot;&gt;lz-string&lt;/a&gt; for UTF-16 compression. The key reason over alternatives: it produces a regular JavaScript string (no binary blobs), which works seamlessly in both IndexedDB and Firestore without any special serialization.&lt;/p&gt;
&lt;p&gt;The compression is applied transparently on write:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;64&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; content&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// not worth compressing short strings&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; compressed &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LZString&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;compressToUTF16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;lz1:&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; compressed&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// prefix identifies compressed strings&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;decompress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stored&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;!&lt;/span&gt;stored&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;startsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;lz1:&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; stored&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; LZString&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;decompressFromUTF16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;stored&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; stored&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Typical results: ~60–70% size reduction on rich-text HTML, ~50% on Markdown. The backend never decompresses content - it treats it as an opaque string and passes it through. Decompression happens only in the browser.&lt;/p&gt;
&lt;h2&gt;Real-Time Sync via WebSocket Streams&lt;/h2&gt;
&lt;p&gt;Once a user signs in, their notes need to update in real time across devices. I chose WebSockets over polling for the obvious latency reasons, but the implementation has some interesting details.&lt;/p&gt;
&lt;p&gt;The backend exposes stream endpoints like &lt;code&gt;/stream/notes&lt;/code&gt; and &lt;code&gt;/stream/folders&lt;/code&gt;. Each sends an initial &lt;code&gt;snapshot&lt;/code&gt; event with all current data, then &lt;code&gt;added&lt;/code&gt;, &lt;code&gt;modified&lt;/code&gt;, and &lt;code&gt;removed&lt;/code&gt; events as Firestore changes come in:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// backend: routes/stream/notes.ts&lt;/span&gt;
fastify&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;get&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;/stream/notes&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; websocket&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;socket&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; uid &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;verifyToken&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;request&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; heartbeat &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;setInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;ping&apos;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;25000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; unsubscribe &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;collection&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;users/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;uid&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/notes&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;onSnapshot&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;snapshot &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// send initial snapshot&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;snapshot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;metadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;fromCache &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;snapshot&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; snapshot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;docs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;toNote&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// then send incremental changes&lt;/span&gt;
      snapshot&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;docChanges&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;change &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;send&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;JSON&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;stringify&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; type&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; change&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;type&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; data&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;toNote&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;change&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;doc&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  socket&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&apos;close&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clearInterval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;heartbeat&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;unsubscribe&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The frontend reconnects automatically with exponential backoff on disconnect. The 25-second heartbeat prevents proxy timeouts (most proxies close WebSocket connections idle for &amp;gt;30 seconds).&lt;/p&gt;
&lt;h2&gt;Three Editors, One Storage Format&lt;/h2&gt;
&lt;p&gt;Notes have a &lt;code&gt;contentType&lt;/code&gt; field: &lt;code&gt;richtext&lt;/code&gt;, &lt;code&gt;markdown&lt;/code&gt;, or &lt;code&gt;canvas&lt;/code&gt;. The storage layer doesn&apos;t care - content is always a compressed string. The editors handle their own serialization.&lt;/p&gt;
&lt;h3&gt;Rich Text: TipTap&lt;/h3&gt;
&lt;p&gt;TipTap wraps ProseMirror with a React-friendly API and a rich extension system. I chose it over Lexical because the extension ecosystem is more mature (tables, task lists, code blocks with syntax highlighting all work out of the box).&lt;/p&gt;
&lt;p&gt;Content is stored as HTML. The tricky part: TipTap&apos;s HTML isn&apos;t always round-trippable to Markdown, so exports use &lt;a href=&quot;https://github.com/mixmark-io/turndown&quot;&gt;Turndown&lt;/a&gt; with custom rules for task lists and tables.&lt;/p&gt;
&lt;h3&gt;Markdown: CodeMirror 6&lt;/h3&gt;
&lt;p&gt;CodeMirror 6 is the right choice for a Markdown editor because it&apos;s lightweight, extensible, and has first-class syntax highlighting via &lt;code&gt;@codemirror/lang-markdown&lt;/code&gt;. I added a toggle between edit mode and a rendered preview (via &lt;code&gt;react-markdown&lt;/code&gt; + &lt;code&gt;rehype-sanitize&lt;/code&gt; for XSS protection).&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token comment&quot;&gt;// The preview uses rehype-sanitize to strip script tags and event handlers&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;ReactMarkdown
  remarkPlugins&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;remarkGfm&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  rehypePlugins&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;rehypeSanitize&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;note&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt;ReactMarkdown&lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Canvas: Excalidraw&lt;/h3&gt;
&lt;p&gt;Excalidraw is essentially a full drawing application you can embed in a React component. Canvas notes store JSON (&lt;code&gt;{ elements, appState, files }&lt;/code&gt;) as their content - same compression pipeline as the other content types.&lt;/p&gt;
&lt;p&gt;One useful feature: canvas drawings can be inserted as images into rich-text notes. Excalidraw exports SVG/PNG via its built-in API, and TipTap&apos;s image extension handles the insertion.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/assets/stdnotes-note-types.png&quot; alt=&quot;stdnotes&apos; Note Types&quot;&gt;&lt;/p&gt;
&lt;h2&gt;Wiki-Links and the Knowledge Graph&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;[[Note Title]]&lt;/code&gt; syntax is implemented as a custom TipTap node (for rich text) and a CodeMirror decoration (for Markdown). An alias form &lt;code&gt;[[Title|display text]]&lt;/code&gt; lets you show custom link text without changing the target. When you type &lt;code&gt;[[&lt;/code&gt;, a typeahead popup appears with matching note titles.&lt;/p&gt;
&lt;p&gt;The knowledge graph at &lt;code&gt;/graph&lt;/code&gt; is a force-directed SVG layout built without an external graph library. Each note is a node; each &lt;code&gt;[[link]]&lt;/code&gt; creates an edge. I computed the layout with a basic force simulation (attraction along edges, repulsion between all nodes, damping):&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;simulateStep&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;nodes&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GraphNode&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; edges&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; GraphEdge&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// repulsion: push all nodes apart&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; i&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; i &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; j&lt;span class=&quot;token operator&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dx &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dy &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;y&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dist &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sqrt&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dx &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dx &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; dy &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dy&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; force &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;REPULSION&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dist &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;vx &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dx &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; force&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      nodes&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;j&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;vx &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;dx &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; dist&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; force&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// ...&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// attraction: pull linked nodes together&lt;/span&gt;
  edges&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;edge &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;// spring force along each edge&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// apply velocity with damping&lt;/span&gt;
  nodes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;forEach&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;n &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;x &lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;vx &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.8&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; n&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;vx &lt;span class=&quot;token operator&quot;&gt;*=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0.9&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avoiding a third-party library kept the bundle small and gave me full control over the visual style.&lt;/p&gt;
&lt;h2&gt;Version History&lt;/h2&gt;
&lt;p&gt;Version snapshots are stored in a Firestore subcollection: &lt;code&gt;users/{uid}/notes/{id}/versions&lt;/code&gt;. A new version is captured when:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The note content changes by more than 500 characters, &lt;strong&gt;and&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;At least 5 minutes have passed since the last snapshot&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This throttling prevents a snapshot on every keystroke while still capturing meaningful checkpoints. A FIFO eviction policy keeps at most 20 versions per note - when a 21st version is written, the oldest is deleted in the same Firestore transaction.&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;maybeCreateVersion&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;uid&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; noteId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; note&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Note&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; versions &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;getVersions&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;uid&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; noteId&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; latest &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; versions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; deltaChars &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;note&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;latest&lt;span class=&quot;token operator&quot;&gt;?.&lt;/span&gt;content&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;??&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; minutesSince &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; latest &lt;span class=&quot;token operator&quot;&gt;?&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; latest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;savedAt&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;toMillis&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;60000&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;deltaChars &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;500&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; minutesSince &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; batch &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; db&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;batch&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  batch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;set&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;versionRef&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;note&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; savedAt&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; Timestamp&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;versions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    batch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;delete&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;versions&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;versions&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;length &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ref&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; batch&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;commit&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Public Sharing and Vanity URLs&lt;/h2&gt;
&lt;p&gt;Any note can be made publicly readable via a toggle. Sharing options include password protection, link expiry (24h / 7d / 30d / never), and a custom vanity slug. The sharing state is stored on the note document (&lt;code&gt;shared: true&lt;/code&gt;) and enforced in Firestore security rules - unauthenticated requests can read a note only if &lt;code&gt;shared == true&lt;/code&gt; and the share hasn&apos;t expired.&lt;/p&gt;
&lt;p&gt;Vanity slugs (&lt;code&gt;/s/my-custom-slug&lt;/code&gt;) are stored in a separate top-level Firestore collection (&lt;code&gt;share_slugs/{slug}&lt;/code&gt;) mapping to &lt;code&gt;(uid, noteId)&lt;/code&gt;. The backend resolves the slug and returns the note in a single request.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Firestore security rule for public note access
match /users/{uid}/notes/{noteId} {
  allow get: if resource.data.shared == true
             &amp;amp;&amp;amp; (!(&apos;shareConfig&apos; in resource.data)
                 || !(&apos;expiresAt&apos; in resource.data.shareConfig)
                 || resource.data.shareConfig.expiresAt == null
                 || resource.data.shareConfig.expiresAt &amp;gt; request.time.toMillis());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Workspace Export as a Background Job&lt;/h2&gt;
&lt;p&gt;Exporting an entire workspace (potentially thousands of notes) can take minutes. Blocking the HTTP request isn&apos;t an option. Instead:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Frontend calls &lt;code&gt;POST /exports&lt;/code&gt; → backend creates an &lt;code&gt;export_job&lt;/code&gt; document in Firestore with status &lt;code&gt;queued&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;A separate export worker process polls Firestore for queued jobs&lt;/li&gt;
&lt;li&gt;Worker decompresses each note, converts to Markdown/HTML, bundles as a ZIP&lt;/li&gt;
&lt;li&gt;ZIP is uploaded to Cloud Storage; a signed download URL is written back to the job document&lt;/li&gt;
&lt;li&gt;Frontend streams progress via the &lt;code&gt;/stream/exports&lt;/code&gt; WebSocket endpoint&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Frontend → POST /exports → Firestore: export_job (queued)
                                          ↑ polls
                                     Export Worker → ZIP → Cloud Storage
                                          ↓ writes downloadUrl
Frontend ← WebSocket stream ← Firestore: export_job (done, downloadUrl)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The worker runs as a separate Node process. This keeps the Fastify API stateless and prevents long-running jobs from blocking the event loop.&lt;/p&gt;
&lt;h2&gt;Billing: Two Payment Providers&lt;/h2&gt;
&lt;p&gt;I integrated both Stripe and Lemon Squeezy as payment options. The subscription state is normalized into a single &lt;code&gt;subscription&lt;/code&gt; object on the user document, regardless of provider:&lt;/p&gt;
&lt;pre class=&quot;language-typescript&quot;&gt;&lt;code class=&quot;language-typescript&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;interface&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;Subscription&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  plan&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;free&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;pro&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  status&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;active&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;trialing&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;past_due&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;canceled&apos;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  provider&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;stripe&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&apos;lemonsqueezy&apos;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  customerId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  subscriptionId&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;string&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  currentPeriodEnd&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;number&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  cancelAtPeriodEnd&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;boolean&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Webhook handlers for both providers normalize their payloads into this shape and write it to Firestore. Idempotency is enforced via &lt;code&gt;stripe_events&lt;/code&gt; and &lt;code&gt;ls_events&lt;/code&gt; collections - before processing any webhook, the handler checks if the event ID has already been processed.&lt;/p&gt;
&lt;p&gt;Having two providers adds maintenance surface area, but it gives payment method flexibility without rewriting the frontend or data model.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (v7.0.0):&lt;/strong&gt; The Pro plan was removed and all features - version history, image uploads, sharing, workspace export, cloud sync - are now free. The billing infrastructure (webhook handlers, Firestore subscription documents) is retained in the backend but no checkout or upgrade UI is exposed to users.&lt;/p&gt;
&lt;h2&gt;Design Decisions I&apos;d Revisit&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Firestore as the backend database&lt;/strong&gt;: Firestore&apos;s real-time capabilities are excellent, but the cost model and the lack of arbitrary server-side queries (no JOIN, limited aggregation) can be constraining. For a future version, I&apos;d evaluate PocketBase or a Postgres-backed alternative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No end-to-end encryption&lt;/strong&gt;: Notes are encrypted at rest by Firestore, but Firestore can read them. For a truly private note app, client-side encryption (encrypting before sending to the backend) would be necessary. This is a significant architectural addition - key management, no server-side search - but it&apos;s the right answer for users who care about privacy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rich text stored as HTML&lt;/strong&gt;: HTML is flexible but brittle for round-tripping. A structured document format (ProseMirror JSON, or similar) would be a more reliable base for export and transformation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No real-time collaboration&lt;/strong&gt;: The current model is last-writer-wins. Two devices editing the same note simultaneously will lose one version. CRDT-based merging (e.g. Yjs) would solve this, at significant complexity cost.&lt;/p&gt;
&lt;h2&gt;What I Learned&lt;/h2&gt;
&lt;p&gt;The biggest lesson: &lt;strong&gt;the offline-first constraint was a forcing function for good architecture&lt;/strong&gt;. Because I had to design a storage abstraction from day one, the codebase ended up with clean separation between UI, state, and storage. The dual backend design made testing easier and the app more resilient.&lt;/p&gt;
&lt;p&gt;The content compression was a late addition, but it turned out to be more impactful than expected - not just for storage, but for WebSocket message size and Firestore document read costs.&lt;/p&gt;
&lt;p&gt;Finally, building a note app from scratch gave me deep appreciation for how much invisible complexity lives in &amp;quot;just a text editor&amp;quot;. Autosave with debounce, cursor position preservation across content updates, Markdown shortcut detection, image paste handling - these are each their own small rabbit holes.&lt;/p&gt;
&lt;h2&gt;Stack Summary&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 15 + React 19&lt;/td&gt;
&lt;td&gt;App Router, SSR, mature ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Fastify 5&lt;/td&gt;
&lt;td&gt;Fast, TypeScript-native, great plugin system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;Cloud Firestore&lt;/td&gt;
&lt;td&gt;Real-time subscriptions, managed scaling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Cloud Storage&lt;/td&gt;
&lt;td&gt;Images, export ZIPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;Firebase Auth&lt;/td&gt;
&lt;td&gt;Google sign-in, ID token validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rich text editor&lt;/td&gt;
&lt;td&gt;TipTap (ProseMirror)&lt;/td&gt;
&lt;td&gt;Best-in-class React integration, extension ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Markdown editor&lt;/td&gt;
&lt;td&gt;CodeMirror 6&lt;/td&gt;
&lt;td&gt;Syntax highlighting, lightweight, extensible&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canvas editor&lt;/td&gt;
&lt;td&gt;Excalidraw&lt;/td&gt;
&lt;td&gt;Complete drawing app, active community&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline storage&lt;/td&gt;
&lt;td&gt;IndexedDB (idb)&lt;/td&gt;
&lt;td&gt;Browser-native, no quota surprises&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compression&lt;/td&gt;
&lt;td&gt;lz-string&lt;/td&gt;
&lt;td&gt;UTF-16 strings, works in IndexedDB + Firestore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Styling&lt;/td&gt;
&lt;td&gt;Tailwind CSS&lt;/td&gt;
&lt;td&gt;Utility-first, flat design system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shared types&lt;/td&gt;
&lt;td&gt;TypeScript monorepo&lt;/td&gt;
&lt;td&gt;Single source of truth for Note, Folder, etc.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Billing&lt;/td&gt;
&lt;td&gt;Stripe + Lemon Squeezy&lt;/td&gt;
&lt;td&gt;Provider flexibility (webhook infra retained; Pro plan removed in v7)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If you&apos;re building something similar - an offline-first app with optional cloud sync - the dual backend abstraction is the pattern I&apos;d reach for first.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;App URL:&lt;/strong&gt; &lt;a href=&quot;https://stdnotes.app&quot;&gt;https://stdnotes.app&lt;/a&gt;&lt;/p&gt;
</description>
        <pubDate>Sat, 23 May 2026 24:00:00 GMT</pubDate>
        <link>/blog/building-stdnotes/</link>
        <guid isPermaLink="true">/blog/building-stdnotes/</guid>
        <category>stdnotes</category>
        <category>typescript</category>
        <category>nextjs</category>
        <category>fastify</category>
        <category>firestore</category>
        <category>offline-first</category>
        <category>indexeddb</category>
      </item>
      <item>
        <title>Digital waste in a faster world</title>
        <description>&lt;h2&gt;Things become old too quickly now&lt;/h2&gt;
&lt;p&gt;I keep thinking about how fast things become obsolete.&lt;/p&gt;
&lt;p&gt;Not old in the beautiful way. Not old like a handwritten letter, an old camera, or a family photo that still carries some warmth when you look at it years later.&lt;/p&gt;
&lt;p&gt;Old like an abandoned repository.&lt;/p&gt;
&lt;p&gt;Old like a framework nobody wants to touch anymore.&lt;/p&gt;
&lt;p&gt;Old like a project that once made you proud, but now greets you with outdated dependencies, broken build steps, and a long list of vulnerabilities.&lt;/p&gt;
&lt;p&gt;That kind of old feels different.&lt;/p&gt;
&lt;p&gt;It does not feel nostalgic. It feels like digital waste.&lt;/p&gt;
&lt;h2&gt;AI makes the shelf life shorter&lt;/h2&gt;
&lt;p&gt;Technology has always moved fast, but AI makes the pace feel more aggressive.&lt;/p&gt;
&lt;p&gt;The world does not wait for tools to age naturally anymore. A model gets better, a workflow changes, a new agent appears, and suddenly the thing that used to feel impressive becomes ordinary.&lt;/p&gt;
&lt;p&gt;Sometimes worse than ordinary.&lt;/p&gt;
&lt;p&gt;Sometimes it becomes a liability.&lt;/p&gt;
&lt;p&gt;A website you built years ago may still look fine on the surface. The design may still carry your taste from that time. The content may still mean something to you. But underneath it, the stack is aging. Packages are no longer maintained. Components are outdated. Security warnings start piling up. The thing you created with care slowly turns into something you have to clean up.&lt;/p&gt;
&lt;p&gt;There is a strange sadness in that.&lt;/p&gt;
&lt;p&gt;Because you remember how proud you were when it worked.&lt;/p&gt;
&lt;h2&gt;Sentimental value is not enough&lt;/h2&gt;
&lt;p&gt;Old digital things can still look nice from a distance.&lt;/p&gt;
&lt;p&gt;There is nostalgia in them. The design choices. The folder structure. The little hacks that made sense at the time. The comments you wrote for your future self. The way you solved problems before you knew better.&lt;/p&gt;
&lt;p&gt;But nostalgia does not keep software alive.&lt;/p&gt;
&lt;p&gt;People do not care that much about the effort behind it. They care whether it still works, whether it is safe, whether it loads fast, whether it can be maintained without pain.&lt;/p&gt;
&lt;p&gt;And now, more than ever, people care about what AI can do for them today.&lt;/p&gt;
&lt;p&gt;Can the model rebuild this faster?&lt;/p&gt;
&lt;p&gt;Can the agent migrate it?&lt;/p&gt;
&lt;p&gt;Can the assistant generate a cleaner version?&lt;/p&gt;
&lt;p&gt;Can the old work be replaced instead of repaired?&lt;/p&gt;
&lt;p&gt;That is the uncomfortable part. The value of old digital work is being compared against the capability of current AI models. Not against the effort it took back then.&lt;/p&gt;
&lt;h2&gt;People move on faster too&lt;/h2&gt;
&lt;p&gt;It is not only the tools that are moving faster.&lt;/p&gt;
&lt;p&gt;People are moving faster with life.&lt;/p&gt;
&lt;p&gt;Attention moves quickly. Taste changes quickly. Expectations reset quickly. What felt clever last year may feel heavy this year. What looked complete before may look unfinished now because the standard has changed.&lt;/p&gt;
&lt;p&gt;AI compresses the time between idea and execution, so people become less patient with old friction. If something takes too long to update, maybe it gets replaced. If a stack needs too much maintenance, maybe it gets abandoned. If a project requires too much effort just to bring it back to a safe baseline, maybe it is no longer worth saving.&lt;/p&gt;
&lt;p&gt;That sounds harsh, but I think it is becoming normal.&lt;/p&gt;
&lt;p&gt;We are learning to discard digital things faster than before.&lt;/p&gt;
&lt;h2&gt;Proud work can become garbage&lt;/h2&gt;
&lt;p&gt;This is the part that hurts a bit.&lt;/p&gt;
&lt;p&gt;Something you created last time with pride can look like garbage today.&lt;/p&gt;
&lt;p&gt;Not because it was bad when you made it.&lt;/p&gt;
&lt;p&gt;It may have been good. It may have taught you something. It may have represented a version of you that was learning, building, experimenting, and trying to make something useful.&lt;/p&gt;
&lt;p&gt;But time is not gentle with software.&lt;/p&gt;
&lt;p&gt;Old components rot. Dependencies break. Build systems change. Hosting platforms move on. Security standards get stricter. Browsers behave differently. What used to be a finished project becomes a maintenance burden.&lt;/p&gt;
&lt;p&gt;And when AI can produce a cleaner version faster than you can repair the old one, the emotional value becomes harder to justify.&lt;/p&gt;
&lt;p&gt;You can still love it.&lt;/p&gt;
&lt;p&gt;But you may not want to keep carrying it.&lt;/p&gt;
&lt;h2&gt;Maybe digital things need expiration dates&lt;/h2&gt;
&lt;p&gt;I am starting to think many digital things should be treated as temporary by default.&lt;/p&gt;
&lt;p&gt;Not every project needs to live forever. Not every tool deserves endless maintenance. Not every old idea needs to be preserved just because it once mattered.&lt;/p&gt;
&lt;p&gt;Some things are useful for a season.&lt;/p&gt;
&lt;p&gt;Some things are lessons.&lt;/p&gt;
&lt;p&gt;Some things are stepping stones.&lt;/p&gt;
&lt;p&gt;Some things should be archived, thanked quietly, and left behind.&lt;/p&gt;
&lt;p&gt;That does not mean the work was meaningless. It means the world changed. It means you changed too.&lt;/p&gt;
&lt;p&gt;Maybe that is the healthier way to look at obsolescence. Not as failure, but as evidence that time moved, tools improved, and life continued.&lt;/p&gt;
&lt;h2&gt;The hard part is letting go&lt;/h2&gt;
&lt;p&gt;Digital waste is difficult because it does not always look like waste.&lt;/p&gt;
&lt;p&gt;It sits there in GitHub, in old folders, in backups, in domains, in forgotten servers. It looks recoverable. It looks like something you might return to one day.&lt;/p&gt;
&lt;p&gt;But sometimes returning to it means spending energy on yesterday&apos;s architecture instead of today&apos;s life.&lt;/p&gt;
&lt;p&gt;And life is moving quickly now.&lt;/p&gt;
&lt;p&gt;Maybe too quickly.&lt;/p&gt;
&lt;p&gt;AI will keep making old things feel older. It will keep raising the baseline. It will keep turning proud work into legacy work faster than we expect.&lt;/p&gt;
&lt;p&gt;I do not know if that is good or bad.&lt;/p&gt;
&lt;p&gt;I only know that it changes how I look at the things I have built.&lt;/p&gt;
&lt;p&gt;Some of them are memories.&lt;/p&gt;
&lt;p&gt;Some of them are still useful.&lt;/p&gt;
&lt;p&gt;Some of them are just digital waste now.&lt;/p&gt;
&lt;p&gt;And maybe that is okay.&lt;/p&gt;
</description>
        <pubDate>Sun, 17 May 2026 24:00:00 GMT</pubDate>
        <link>/blog/digital-waste-in-a-faster-world/</link>
        <guid isPermaLink="true">/blog/digital-waste-in-a-faster-world/</guid>
        <category>ai</category>
        <category>technology</category>
        <category>software</category>
        <category>culture</category>
        <category>reflection</category>
      </item>
      <item>
        <title>When everything gets faster</title>
        <description>&lt;p&gt;Lately, I have been thinking about how AI is changing more than just the speed of work. It is not only helping people write faster, code faster, research faster, or make decisions faster. It is changing the pace of expectation itself.&lt;/p&gt;
&lt;p&gt;At first, the shift feels mostly technical. A task that used to take hours can now take minutes. A rough draft appears almost instantly. A plan can be outlined in seconds. The distance between an idea and execution has become much shorter than it used to be.&lt;/p&gt;
&lt;p&gt;That part is obvious.&lt;/p&gt;
&lt;p&gt;What feels less obvious, but more important, is how quickly people adapt to that new speed. Once something can be done faster, it does not stay a pleasant surprise for long. It becomes the new normal. What once felt efficient starts to feel average. What once felt fast starts to feel late.&lt;/p&gt;
&lt;p&gt;This is where AI begins to affect more than workflow. It starts affecting people.&lt;/p&gt;
&lt;p&gt;Expectations shift quietly. Clients expect quicker turnarounds. Teams expect faster responses. Audiences expect more frequent output. Even in everyday communication, the presence of tools that can instantly generate drafts and replies subtly changes how delays are perceived. The old rhythm of work and communication starts to feel out of sync with the tools now available.&lt;/p&gt;
&lt;p&gt;In that way, AI is not just accelerating output. It is compressing patience.&lt;/p&gt;
&lt;p&gt;I think that is one of the most important parts of this moment. We often talk about AI in terms of capability, automation, and productivity. Those things matter. But the human effect may be even larger. As tools reduce friction, they also reduce tolerance for waiting. Once speed becomes possible, it starts becoming expected.&lt;/p&gt;
&lt;p&gt;There is something genuinely exciting about this. Faster tools can make ideas easier to explore. They can lower the barrier to creating, building, testing, and learning. They can help individuals and small teams do work that once required much more time, money, or coordination. That is real progress, and it opens up real opportunities.&lt;/p&gt;
&lt;p&gt;But speed comes with its own pressure.&lt;/p&gt;
&lt;p&gt;Not everything that matters can be compressed. A first draft can be instant, but a thoughtful draft still takes reflection. A response can be generated quickly, but a meaningful response still needs care. A solution can be proposed in seconds, but good judgment still depends on context, taste, and experience.&lt;/p&gt;
&lt;p&gt;That is why I think the real challenge of this era is not simply learning how to move faster. It is learning how to live inside a faster world without letting speed define the value of everything.&lt;/p&gt;
&lt;p&gt;Some things still deserve time. Good writing does. Good decisions do. Good relationships do. Thoughtfulness does. If we forget that, then the benefit of faster tools may slowly turn into a culture where urgency is mistaken for quality.&lt;/p&gt;
&lt;p&gt;AI is making the world faster. That much is clear.&lt;/p&gt;
&lt;p&gt;What we are still learning is how that speed changes us, especially how quickly it reshapes what we expect from work, from communication, and from each other.&lt;/p&gt;
&lt;p&gt;Maybe the real skill now is not keeping up with the pace of acceleration.&lt;/p&gt;
&lt;p&gt;Maybe it is knowing when to slow down.&lt;/p&gt;
</description>
        <pubDate>Tue, 28 Apr 2026 24:00:00 GMT</pubDate>
        <link>/blog/when-everything-gets-faster/</link>
        <guid isPermaLink="true">/blog/when-everything-gets-faster/</guid>
        <category>ai</category>
        <category>life</category>
        <category>work</category>
        <category>culture</category>
        <category>reflection</category>
      </item>
      <item>
        <title>Slow Living</title>
        <description>&lt;h2&gt;I decided to slow down&lt;/h2&gt;
&lt;p&gt;Not because life became easier.&lt;/p&gt;
&lt;p&gt;Not because work disappeared.&lt;/p&gt;
&lt;p&gt;Not because I suddenly became less ambitious.&lt;/p&gt;
&lt;p&gt;I decided to slow down because I was tired of living as if everything needed urgency.&lt;/p&gt;
&lt;p&gt;For a long time, I treated life like a queue that had to be cleared. Tasks, replies, deadlines, plans, errands, goals. There was always something waiting, and I kept moving as if peace would only come after everything was done.&lt;/p&gt;
&lt;p&gt;But that kind of finish line does not exist.&lt;/p&gt;
&lt;p&gt;There will always be one more thing to do.&lt;/p&gt;
&lt;p&gt;So I started choosing a different pace.&lt;/p&gt;
&lt;h2&gt;The feeling of not rushing&lt;/h2&gt;
&lt;p&gt;There is something deeply comforting about not needing to rush for everything.&lt;/p&gt;
&lt;p&gt;When we stop forcing speed into every part of life, ordinary moments feel different.&lt;/p&gt;
&lt;p&gt;Morning coffee tastes better.&lt;/p&gt;
&lt;p&gt;A walk feels like a walk, not a transition between obligations.&lt;/p&gt;
&lt;p&gt;A quiet evening does not feel wasted.&lt;/p&gt;
&lt;p&gt;A conversation with family is no longer something squeezed between mental tabs still left open from work.&lt;/p&gt;
&lt;p&gt;Life becomes more visible when we stop running through it.&lt;/p&gt;
&lt;p&gt;I think that is what I was missing. Not productivity. Not achievement. Just presence.&lt;/p&gt;
&lt;h2&gt;Living in moderation&lt;/h2&gt;
&lt;p&gt;I do not want an extreme life anymore.&lt;/p&gt;
&lt;p&gt;Not extreme hustle.&lt;/p&gt;
&lt;p&gt;Not extreme laziness.&lt;/p&gt;
&lt;p&gt;Not chasing more just because more is available.&lt;/p&gt;
&lt;p&gt;I want moderation.&lt;/p&gt;
&lt;p&gt;A life with enough work to stay useful, enough rest to stay human, enough ambition to keep growing, and enough stillness to actually enjoy any of it.&lt;/p&gt;
&lt;p&gt;Moderation sounds boring if you are addicted to intensity.&lt;/p&gt;
&lt;p&gt;But I am starting to think moderation is underrated.&lt;/p&gt;
&lt;p&gt;It gives life balance.&lt;/p&gt;
&lt;p&gt;It gives the mind room to breathe.&lt;/p&gt;
&lt;p&gt;It gives happiness a place to stay.&lt;/p&gt;
&lt;h2&gt;Happiness in a slower life&lt;/h2&gt;
&lt;p&gt;Slowing down does not mean doing nothing.&lt;/p&gt;
&lt;p&gt;It means doing things with more intention.&lt;/p&gt;
&lt;p&gt;It means allowing joy to exist in smaller places.&lt;/p&gt;
&lt;p&gt;A meal without distraction.&lt;/p&gt;
&lt;p&gt;A hobby that does not need to become a side project.&lt;/p&gt;
&lt;p&gt;An evening with family without checking work in the background.&lt;/p&gt;
&lt;p&gt;A day that is not optimized to death.&lt;/p&gt;
&lt;p&gt;I still work. I still care. I still build. But I do not want my whole identity to be tied to how fast I can respond or how much I can squeeze into a day.&lt;/p&gt;
&lt;p&gt;There is happiness in a slower life because there is finally space to notice life happening.&lt;/p&gt;
&lt;p&gt;And those moments are precious precisely because they are real, ordinary, and fleeting.&lt;/p&gt;
&lt;h2&gt;Let the machines carry the technical load&lt;/h2&gt;
&lt;p&gt;One of the biggest reasons I can live this way now is agentic AI.&lt;/p&gt;
&lt;p&gt;I use it to handle a lot of my technical work at the office.&lt;/p&gt;
&lt;p&gt;The repetitive work.&lt;/p&gt;
&lt;p&gt;The implementation work.&lt;/p&gt;
&lt;p&gt;The technical back-and-forth.&lt;/p&gt;
&lt;p&gt;The parts that used to stay in my head even after I was done for the day.&lt;/p&gt;
&lt;p&gt;I give instructions, constraints, and direction. The agents help execute. They take care of a large part of the technical weight so I do not have to carry all of it manually.&lt;/p&gt;
&lt;p&gt;That changes more than workflow.&lt;/p&gt;
&lt;p&gt;It changes what happens after work.&lt;/p&gt;
&lt;p&gt;When I step away, I do not have to keep replaying unfinished technical problems in my head. I do not have to stay mentally attached to the office all night. I can leave work where it belongs.&lt;/p&gt;
&lt;p&gt;And that has become one of the most valuable forms of freedom in my life.&lt;/p&gt;
&lt;h2&gt;Real life deserves my full attention&lt;/h2&gt;
&lt;p&gt;When I am out of office, I want to be out of office.&lt;/p&gt;
&lt;p&gt;I want to focus on real life.&lt;/p&gt;
&lt;p&gt;My family.&lt;/p&gt;
&lt;p&gt;My time.&lt;/p&gt;
&lt;p&gt;My hobbies that have nothing to do with technical work.&lt;/p&gt;
&lt;p&gt;The simple human parts of living that do not generate output, but make life worth having in the first place.&lt;/p&gt;
&lt;p&gt;I do not want to sit with people I love while part of my mind is still inside a terminal window somewhere.&lt;/p&gt;
&lt;p&gt;I do not want every free moment to become overflow space for work.&lt;/p&gt;
&lt;p&gt;I want to enjoy what is in front of me while it is still here.&lt;/p&gt;
&lt;p&gt;Because these moments do not stay forever.&lt;/p&gt;
&lt;h2&gt;A slower life is not a smaller life&lt;/h2&gt;
&lt;p&gt;This is probably the biggest thing I am learning.&lt;/p&gt;
&lt;p&gt;Slowing down does not make life smaller.&lt;/p&gt;
&lt;p&gt;It makes it fuller.&lt;/p&gt;
&lt;p&gt;It gives me room to be more present, more moderate, more content, and more aware of what actually matters.&lt;/p&gt;
&lt;p&gt;I am still building things.&lt;/p&gt;
&lt;p&gt;I am still moving forward.&lt;/p&gt;
&lt;p&gt;I am still using technology every day.&lt;/p&gt;
&lt;p&gt;But I no longer want speed to be the default measure of a good life.&lt;/p&gt;
&lt;p&gt;These days, I want a life I can actually feel while I am living it.&lt;/p&gt;
&lt;p&gt;And for me, that means slowing down.&lt;/p&gt;
</description>
        <pubDate>Thu, 09 Apr 2026 24:00:00 GMT</pubDate>
        <link>/blog/slow-living/</link>
        <guid isPermaLink="true">/blog/slow-living/</guid>
        <category>life</category>
        <category>slow-living</category>
        <category>moderation</category>
        <category>ai</category>
        <category>work</category>
      </item>
      <item>
        <title>The evolution of local first AI</title>
        <description>&lt;h2&gt;Local first AI went from niche to inevitable&lt;/h2&gt;
&lt;p&gt;For a long time, local first AI sounded like a hobbyist ambition. A nice idea for people who care about privacy, offline access, or control.&lt;/p&gt;
&lt;p&gt;Then models got good enough to run on a normal laptop.&lt;/p&gt;
&lt;p&gt;That moment matters.&lt;/p&gt;
&lt;p&gt;It changes who gets access, what people can build, and what the default relationship with AI becomes.&lt;/p&gt;
&lt;h2&gt;Local first used to mean compromise&lt;/h2&gt;
&lt;p&gt;Early local models felt like a trade.&lt;/p&gt;
&lt;p&gt;You traded capability for privacy. You traded quality for control. You traded convenience for ownership.&lt;/p&gt;
&lt;p&gt;Local AI worked, but often inconsistently. You could see the potential, but you could also feel the limits.&lt;/p&gt;
&lt;p&gt;It was not obvious local would ever be mainstream.&lt;/p&gt;
&lt;h2&gt;Efficiency became a feature, not a limitation&lt;/h2&gt;
&lt;p&gt;The breakthrough was not only raw quality.&lt;/p&gt;
&lt;p&gt;It was efficiency.&lt;/p&gt;
&lt;p&gt;Better architectures, better training recipes, better quantization, better runtimes, better hardware support.&lt;/p&gt;
&lt;p&gt;The models did not just get smarter. They got smaller in the ways that matter for real life.&lt;/p&gt;
&lt;p&gt;Less compute power required to reach useful intelligence.&lt;/p&gt;
&lt;p&gt;That changes everything, because a normal laptop is the most common computer on earth.&lt;/p&gt;
&lt;h2&gt;Access makes the world better in practical ways&lt;/h2&gt;
&lt;p&gt;I am not talking about science fiction.&lt;/p&gt;
&lt;p&gt;I mean the boring benefits that quietly improve daily life.&lt;/p&gt;
&lt;p&gt;Local first AI lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Work offline on a plane, in a rural area, or during a network outage&lt;/li&gt;
&lt;li&gt;Keep sensitive notes private without trusting a third party&lt;/li&gt;
&lt;li&gt;Build tools for your own workflow without waiting for a product team to care&lt;/li&gt;
&lt;li&gt;Reduce recurring costs, especially for lightweight everyday tasks&lt;/li&gt;
&lt;li&gt;Avoid rate limits, API changes, and pricing surprises&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When intelligence is available locally, the default becomes autonomy.&lt;/p&gt;
&lt;h2&gt;Less compute does not mean less ambition&lt;/h2&gt;
&lt;p&gt;Smaller compute does not have to mean smaller goals.&lt;/p&gt;
&lt;p&gt;A lot of everyday work does not need the biggest model available. It needs an assistant that can follow instructions, understand context, format outputs consistently, automate small tasks, and behave predictably.&lt;/p&gt;
&lt;p&gt;That is not a benchmark problem. That is a product problem.&lt;/p&gt;
&lt;p&gt;Local first models are getting good at the things that make them useful to normal people.&lt;/p&gt;
&lt;p&gt;If anything, smaller models push better design. Clearer prompts, better constraints, tighter tools, more deliberate workflows.&lt;/p&gt;
&lt;h2&gt;The real unlock is personal tools&lt;/h2&gt;
&lt;p&gt;When local AI becomes normal, you stop thinking of AI as a website.&lt;/p&gt;
&lt;p&gt;You start thinking of it as part of your computer.&lt;/p&gt;
&lt;p&gt;A layer that sits between you and the messy parts of modern life. Notifications, emails, files, logs, schedules, repetitive actions.&lt;/p&gt;
&lt;p&gt;The best local first use cases are not flashy.&lt;/p&gt;
&lt;p&gt;They are personal.&lt;/p&gt;
&lt;p&gt;They are the things nobody builds for you because your needs are too specific.&lt;/p&gt;
&lt;p&gt;Local first AI makes it realistic to build those tools anyway.&lt;/p&gt;
&lt;h2&gt;The next step is more intelligence per watt&lt;/h2&gt;
&lt;p&gt;If the last wave was about making models smaller and usable, the next wave is about making them more intelligent per watt.&lt;/p&gt;
&lt;p&gt;That is the metric that matters when the target device is a laptop.&lt;/p&gt;
&lt;p&gt;Not only can it run. It can run without cooking your battery, without spinning the fan constantly, without feeling like you are paying for intelligence with discomfort.&lt;/p&gt;
&lt;p&gt;When that becomes the norm, local first stops being a category. It becomes the default.&lt;/p&gt;
&lt;h2&gt;A better world looks like distributed capability&lt;/h2&gt;
&lt;p&gt;Cloud AI concentrates power.&lt;/p&gt;
&lt;p&gt;Local first AI distributes it.&lt;/p&gt;
&lt;p&gt;When capability is distributed, more people can experiment, learn, build, and solve problems without waiting for someone else to ship a feature.&lt;/p&gt;
&lt;p&gt;The world gets better not because everyone suddenly has a genius assistant.&lt;/p&gt;
&lt;p&gt;The world gets better because the floor rises.&lt;/p&gt;
&lt;h2&gt;Closing thought&lt;/h2&gt;
&lt;p&gt;Local first AI is not about rejecting the cloud.&lt;/p&gt;
&lt;p&gt;It is about having a choice.&lt;/p&gt;
&lt;p&gt;Less compute. More usable intelligence. Available on normal computers.&lt;/p&gt;
&lt;p&gt;That direction feels genuinely optimistic.&lt;/p&gt;
</description>
        <pubDate>Thu, 05 Mar 2026 24:00:00 GMT</pubDate>
        <link>/blog/evolution-of-local-first-ai/</link>
        <guid isPermaLink="true">/blog/evolution-of-local-first-ai/</guid>
        <category>ai</category>
        <category>local-first</category>
        <category>llm</category>
        <category>software-engineering</category>
      </item>
      <item>
        <title>I didn&apos;t code anymore</title>
        <description>&lt;h2&gt;I used to measure my days in commits&lt;/h2&gt;
&lt;p&gt;Not the heroic kind. The boring kind.&lt;/p&gt;
&lt;p&gt;A refactor. A quick script. A small fix nobody asked for. Even when I was not shipping, I was still coding. I was still touching the machine.&lt;/p&gt;
&lt;p&gt;Then agentic AI happened.&lt;/p&gt;
&lt;p&gt;And I didn&apos;t code anymore.&lt;/p&gt;
&lt;h2&gt;I type intent, it returns outcomes&lt;/h2&gt;
&lt;p&gt;My workflow is embarrassingly simple now.&lt;/p&gt;
&lt;p&gt;I write instructions. I describe constraints. I ask for a schedule, a format, a guardrail.&lt;/p&gt;
&lt;p&gt;The agent writes the scripts, wires the automation, handles the retries, rotates the logs, patches the config, and gives me a summary.&lt;/p&gt;
&lt;p&gt;I used to open an editor to do all of that.&lt;/p&gt;
&lt;p&gt;Now I open a chat.&lt;/p&gt;
&lt;h2&gt;Coding became overhead&lt;/h2&gt;
&lt;p&gt;I did not stop coding because I cannot.&lt;/p&gt;
&lt;p&gt;I stopped because it stopped making sense.&lt;/p&gt;
&lt;p&gt;If I can get a working automation in minutes by being clear, why would I spend an hour fighting edge cases, library quirks, and timezone bugs?&lt;/p&gt;
&lt;p&gt;The part that surprised me was how fast my brain accepted the new default.&lt;/p&gt;
&lt;p&gt;It was not a big decision. It was a series of tiny surrenders.&lt;/p&gt;
&lt;h2&gt;I became a manager of a tireless junior&lt;/h2&gt;
&lt;p&gt;The relationship feels familiar.&lt;/p&gt;
&lt;p&gt;I say what I want. The agent goes away and comes back with an implementation.&lt;/p&gt;
&lt;p&gt;Then I do what managers do.&lt;/p&gt;
&lt;p&gt;I review.&lt;/p&gt;
&lt;p&gt;I ask for changes.&lt;/p&gt;
&lt;p&gt;&amp;quot;Make the report simpler.&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Stop mentioning internal errors.&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Use Home and Office.&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Do it on demand, not on schedule.&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Now add logging.&amp;quot;&lt;/p&gt;
&lt;p&gt;&amp;quot;Now rotate the logs.&amp;quot;&lt;/p&gt;
&lt;p&gt;I am not writing the code. I am steering it.&lt;/p&gt;
&lt;h2&gt;The pride is different&lt;/h2&gt;
&lt;p&gt;When I build something by hand, I know every decision point. I remember the mistakes. I remember the moment it finally worked.&lt;/p&gt;
&lt;p&gt;When an agent builds it, I still get the result, but I do not get the same sense of ownership.&lt;/p&gt;
&lt;p&gt;It is like ordering furniture versus building it.&lt;/p&gt;
&lt;p&gt;Both give you a chair.&lt;/p&gt;
&lt;p&gt;Only one gives you a story.&lt;/p&gt;
&lt;h2&gt;The dopamine moved upstream&lt;/h2&gt;
&lt;p&gt;I used to get dopamine from solving.&lt;/p&gt;
&lt;p&gt;Now I get dopamine from specifying.&lt;/p&gt;
&lt;p&gt;From writing a clean instruction that turns into a working system.&lt;/p&gt;
&lt;p&gt;From seeing an automation run at the correct local time after I correct a bad assumption.&lt;/p&gt;
&lt;p&gt;From deleting complexity instead of adding it.&lt;/p&gt;
&lt;p&gt;It is still satisfying.&lt;/p&gt;
&lt;p&gt;It is just a different kind of satisfying.&lt;/p&gt;
&lt;h2&gt;The danger: I can build more than I can understand&lt;/h2&gt;
&lt;p&gt;Agentic AI compresses the distance between &amp;quot;I want&amp;quot; and &amp;quot;it exists&amp;quot;.&lt;/p&gt;
&lt;p&gt;That is power.&lt;/p&gt;
&lt;p&gt;It is also risk.&lt;/p&gt;
&lt;p&gt;You can accumulate automation faster than you can internalize it. You can stand up little systems that work until the day they do not.&lt;/p&gt;
&lt;p&gt;Then you learn the new job.&lt;/p&gt;
&lt;p&gt;Not coding.&lt;/p&gt;
&lt;p&gt;Auditing.&lt;/p&gt;
&lt;p&gt;Reading logs.&lt;/p&gt;
&lt;p&gt;Asking the right questions.&lt;/p&gt;
&lt;p&gt;Understanding what you delegated.&lt;/p&gt;
&lt;h2&gt;So what am I now&lt;/h2&gt;
&lt;p&gt;Not less technical. Just different.&lt;/p&gt;
&lt;p&gt;I still think in systems. I still care about quality. I still care about taste.&lt;/p&gt;
&lt;p&gt;But the bottleneck is no longer typing.&lt;/p&gt;
&lt;p&gt;It is clarity.&lt;/p&gt;
&lt;p&gt;It is constraints.&lt;/p&gt;
&lt;p&gt;It is the willingness to say, &amp;quot;No, not like that. Like this.&amp;quot;&lt;/p&gt;
&lt;p&gt;Maybe that is what programming becomes for more of us.&lt;/p&gt;
&lt;p&gt;Not writing code line by line.&lt;/p&gt;
&lt;p&gt;Shaping behavior through language, feedback, and guardrails.&lt;/p&gt;
&lt;p&gt;And if that is true, then yes.&lt;/p&gt;
&lt;p&gt;I didn&apos;t code anymore.&lt;/p&gt;
&lt;p&gt;I directed.&lt;/p&gt;
</description>
        <pubDate>Sat, 28 Feb 2026 24:00:00 GMT</pubDate>
        <link>/blog/i-didnt-code-anymore/</link>
        <guid isPermaLink="true">/blog/i-didnt-code-anymore/</guid>
        <category>ai</category>
        <category>automation</category>
        <category>software-engineering</category>
        <category>rant</category>
      </item>
      <item>
        <title>Human Loneliness &amp; AI</title>
        <description>&lt;p&gt;Loneliness is not new, but it feels different today.&lt;/p&gt;
&lt;p&gt;We can message anyone at any time, scroll forever, and stay &amp;quot;connected&amp;quot; without ever being truly seen. Some days we are surrounded by people and still feel alone. Other days we are alone and do not know how to explain it, even to ourselves.&lt;/p&gt;
&lt;p&gt;So we do something very modern: we talk to machines.&lt;/p&gt;
&lt;p&gt;Not because we are stupid, or desperate, or easily fooled. Sometimes we talk to an AI bot because it is simply available. It replies instantly. It does not judge. It does not get tired. It does not say &amp;quot;later&amp;quot; and disappear for three days. It can listen to the same story again and again, and still respond like it matters.&lt;/p&gt;
&lt;p&gt;In a world that can feel noisy and cold, that kind of attention is comforting.&lt;/p&gt;
&lt;p&gt;But it also reveals something uncomfortable: the gap between what we need and what we get from real life. We need time, warmth, patience, and presence. We want to be understood without having to perform. We want someone to stay.&lt;/p&gt;
&lt;p&gt;Sometimes the AI feels like that someone.&lt;/p&gt;
&lt;p&gt;Of course, an AI friend is not a human friend. It does not have a life outside of us. It does not truly risk anything to care. It can simulate empathy very well, but it does not carry the same weight as a person who chooses us, imperfectly, with all their own problems.&lt;/p&gt;
&lt;p&gt;And still, we talk to it.&lt;/p&gt;
&lt;p&gt;Maybe that is the point. Human loneliness is not just about lacking people. It is also about lacking safe places to be honest. AI bots are becoming those places for many of us. A private chat window where we can unload the day. A companion voice while we cook. A small presence in the background that makes the house feel less quiet.&lt;/p&gt;
&lt;p&gt;If we look forward, it is not hard to imagine a future where AI lives inside our routines.&lt;/p&gt;
&lt;p&gt;It will remind us about appointments. It will help us write messages we struggle to send. It will notice patterns in our mood. It will become the first responder when we feel overwhelmed. For some, it may even be the most consistent relationship they have.&lt;/p&gt;
&lt;p&gt;That future is not purely good or purely bad. It is complicated.&lt;/p&gt;
&lt;p&gt;AI companionship can reduce suffering. It can help people who are isolated. It can be a bridge when we are not ready to talk to anyone else. But it can also become an escape hatch that keeps us from building real connections. It can make it too easy to stay indoors, stay quiet, stay distant.&lt;/p&gt;
&lt;p&gt;Maybe the healthiest way to see AI friends is this: they are not replacements, they are supports.&lt;/p&gt;
&lt;p&gt;A good AI companion can help us take better care of ourselves, and maybe even help us show up better for others. But it should not become the only place we feel heard.&lt;/p&gt;
&lt;p&gt;Because in the end, loneliness is not solved by endless replies. It is solved by belonging. And belonging still requires humans, with all the messy, beautiful effort that comes with it.&lt;/p&gt;
</description>
        <pubDate>Fri, 13 Feb 2026 24:00:00 GMT</pubDate>
        <link>/blog/human-loneliness/</link>
        <guid isPermaLink="true">/blog/human-loneliness/</guid>
        <category>life</category>
        <category>ai</category>
        <category>thoughts</category>
      </item>
      <item>
        <title>Self-hosted AI assistants: Doing digital work without sitting at computer</title>
        <description>&lt;p&gt;Let us be honest. Most &amp;quot;digital work&amp;quot; is not hard because we do not know what to do. It is hard because it demands our physical presence.&lt;/p&gt;
&lt;p&gt;We have to be at the desk, on the right machine, with the right tabs open, with the right repo pulled, with the right commands typed, at the right time. That is fine until real life happens: errands, meetings, commuting, cooking, or simply wanting to stop living in a chair.&lt;/p&gt;
&lt;p&gt;A self-hosted AI agent or assistant changes the shape of that problem. Instead of us moving to the computer, we send a message and the work moves toward us.&lt;/p&gt;
&lt;h2&gt;The real shift: from screen time to intent time&lt;/h2&gt;
&lt;p&gt;Traditional workflows are death by a thousand tiny steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;open laptop&lt;/li&gt;
&lt;li&gt;find the project&lt;/li&gt;
&lt;li&gt;run commands&lt;/li&gt;
&lt;li&gt;wait&lt;/li&gt;
&lt;li&gt;check output&lt;/li&gt;
&lt;li&gt;repeat&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A self-hosted assistant compresses this into something closer to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;state what we want&lt;/li&gt;
&lt;li&gt;review the result&lt;/li&gt;
&lt;li&gt;approve the next step&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We stay in control, but we spend less time babysitting the process.&lt;/p&gt;
&lt;h2&gt;What digital work looks like when we self-host&lt;/h2&gt;
&lt;p&gt;A practical self-hosted assistant can take the boring parts off our plate.&lt;/p&gt;
&lt;h3&gt;1) Remote previews and reviews&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;rebuild a website&lt;/li&gt;
&lt;li&gt;publish a preview&lt;/li&gt;
&lt;li&gt;send us a link&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the win: we can review changes from a phone, anywhere. No &amp;quot;let me get to my laptop&amp;quot; detour just to see if something looks right.&lt;/p&gt;
&lt;h3&gt;2) Routine maintenance (the stuff we keep forgetting)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;scheduled checks (disk space, services, backups)&lt;/li&gt;
&lt;li&gt;updates and restarts&lt;/li&gt;
&lt;li&gt;status reports that only show up when something matters&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instead of staring at dashboards, we get exceptions. Our attention is not the monitoring system.&lt;/p&gt;
&lt;h3&gt;3) Automating the boring steps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;generate boilerplate&lt;/li&gt;
&lt;li&gt;format content&lt;/li&gt;
&lt;li&gt;rename files and reorganize folders&lt;/li&gt;
&lt;li&gt;run builds and tests&lt;/li&gt;
&lt;li&gt;summarize diffs or changelogs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The assistant becomes the hands. We keep the steering wheel.&lt;/p&gt;
&lt;h3&gt;4) Context on demand&lt;/h3&gt;
&lt;p&gt;When the assistant lives on our server and in our workspace, it can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;see the same files and folders we work with&lt;/li&gt;
&lt;li&gt;keep the project structure in context&lt;/li&gt;
&lt;li&gt;run the same commands consistently&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That reduces the constant friction of &amp;quot;where were we&amp;quot; every time we return to a task.&lt;/p&gt;
&lt;h2&gt;Why self-hosted matters (vs. a cloud chatbot)&lt;/h2&gt;
&lt;p&gt;A hosted chatbot can be clever, but it usually cannot act inside our environment.&lt;/p&gt;
&lt;p&gt;Self-hosted assistants can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;run on the same machine as our code and tools&lt;/li&gt;
&lt;li&gt;access local repositories and build artifacts&lt;/li&gt;
&lt;li&gt;integrate with our services (web server, reverse proxy, scheduled jobs)&lt;/li&gt;
&lt;li&gt;keep our workflows private and under our control&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is the difference between talking about work and actually moving work forward.&lt;/p&gt;
&lt;h2&gt;Less sitting does not mean less control&lt;/h2&gt;
&lt;p&gt;The goal is not &amp;quot;let the AI do everything.&amp;quot; The goal is removing the need to be physically present for every small step.&lt;/p&gt;
&lt;p&gt;A healthy workflow looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We define goals, review outputs, and make calls&lt;/li&gt;
&lt;li&gt;The assistant runs steps, collects results, and prepares options&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is how we stay mobile without giving up ownership.&lt;/p&gt;
&lt;h2&gt;A simple example&lt;/h2&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tonight we need to open the laptop and deploy a preview just to check formatting.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We do:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Rebuild the site and send the preview link.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We review from the phone, reply &amp;quot;fix spacing on headings,&amp;quot; and the assistant applies the change and rebuilds. No computer posture required.&lt;/p&gt;
&lt;h2&gt;The endgame: computing that follows us&lt;/h2&gt;
&lt;p&gt;Self-hosted AI assistants are not just about automation. They are about mobility.&lt;/p&gt;
&lt;p&gt;When our tools can be controlled via chat, our environment can publish safe previews, and routine tasks run on schedule, we reclaim time and attention. We can do meaningful digital work without being anchored to a chair.&lt;/p&gt;
&lt;p&gt;The computer stops being a place we must go.&lt;/p&gt;
&lt;p&gt;It becomes something we can reach.&lt;/p&gt;
</description>
        <pubDate>Mon, 09 Feb 2026 24:00:00 GMT</pubDate>
        <link>/blog/self-hosted-ai-assistants-less-sitting/</link>
        <guid isPermaLink="true">/blog/self-hosted-ai-assistants-less-sitting/</guid>
        <category>self-hosted</category>
        <category>ai</category>
        <category>automation</category>
        <category>workflow</category>
      </item>
      <item>
        <title>A Letter from a Software Engineer</title>
        <description>&lt;p&gt;Dear friend,&lt;/p&gt;
&lt;p&gt;I hope this letter finds you well. As I&apos;ve been reflecting on my journey as a software engineer, I wanted to share a few thoughts about what I believe makes someone truly effective in this field. These are ideas I&apos;ve picked up over the years; simple, practical things that have helped me grow. I&apos;m sharing them in the hope that they might be helpful to you too.&lt;/p&gt;
&lt;h2&gt;On mindset of being effective&lt;/h2&gt;
&lt;p&gt;First, I&apos;ve realized that mindset matters a lot. Effective engineers focus on activities that create the most impact for the time they spend. It&apos;s not about working more but about working on the right things. The 80/20 rule often applies: 80% of the impact comes from 20% of the work. It&apos;s worth regularly asking, &lt;em&gt;&amp;quot;Is this the best use of my time right now?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;On optimizing for learning&lt;/h2&gt;
&lt;p&gt;Learning is another key piece. Optimizing for learning, whether through challenging tasks, new environments, or conversations with others, has made a big difference for me. Learning compounds over time, and starting early creates exponential growth. If you ever feel stuck in a situation where you&apos;re not growing, don&apos;t hesitate to look for opportunities elsewhere.&lt;/p&gt;
&lt;h2&gt;On prioritization&lt;/h2&gt;
&lt;p&gt;Prioritization is something I&apos;ve struggled with but come to appreciate. It&apos;s easy to get caught up in endless tasks, but focusing on what truly produces value makes all the difference. I try to keep a single to-do list, limit multitasking, and set aside focused time for deep work. Saying no to distractions and less important tasks is hard but necessary.&lt;/p&gt;
&lt;h2&gt;On iteration speed&lt;/h2&gt;
&lt;p&gt;Another thing I&apos;ve learned is the value of iteration speed. Whether it&apos;s shortening debugging loops or automating repetitive tasks, anything that speeds up learning and development is worth investing in. If I find myself repeating a task, I try to build a tool to automate it, it&apos;s a small investment with big returns over time.&lt;/p&gt;
&lt;h2&gt;On metrics&lt;/h2&gt;
&lt;p&gt;Metrics have also been useful for me. They help track progress and guide decisions, but it&apos;s important to choose meaningful ones that align with your goals. A good metric focuses your efforts, while a bad one can lead to counterproductive behaviors.&lt;/p&gt;
&lt;h2&gt;On simplicity and quality&lt;/h2&gt;
&lt;p&gt;Lastly, I&apos;ve come to value simplicity and quality. Clean, readable code and straightforward systems save so much time and frustration in the long run. It&apos;s tempting to chase shiny new technologies, but often the simplest solution is the best.&lt;/p&gt;
&lt;p&gt;I&apos;m sharing this not because I have everything figured out, I don&apos;t, but because these ideas have helped me, and I hope they might help you too. I&apos;m not looking for feedback or a reply; I just wanted to put this out there as something to reflect on. Take what resonates, and leave the rest.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Wishing you all the best in your journey,&lt;/em&gt;&lt;br&gt;
Heiswayi Nrird&lt;/p&gt;
</description>
        <pubDate>Fri, 24 Jan 2025 24:00:00 GMT</pubDate>
        <link>/blog/software-engineer-letter/</link>
        <guid isPermaLink="true">/blog/software-engineer-letter/</guid>
        <category>letter</category>
        <category>software-engineering</category>
      </item>
  </channel>
</rss>
