<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Jack Krone: Projects</title>
    <link>https://jackkrone.com/projects/</link>
    <description>Posts from the Projects section of jackkrone.com.</description>
    <language>en</language>
    <atom:link href="https://jackkrone.com/projects/rss.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Letting my plants email my roommates while I travel</title>
      <link>https://jackkrone.com/projects/letting-my-plants-email-my-roommates/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/letting-my-plants-email-my-roommates/</guid>
      <pubDate>Fri, 24 Jul 2026 04:13:32 GMT</pubDate>
      <description>An agent-built email service that reminds my roommates when to water my plants while I travel, each plant asking in its own voice.</description>
      <content:encoded><![CDATA[<figure><a href="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-1147.jpg"><img src="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-800.jpg" srcset="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-200.jpg 200w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-400.jpg 400w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-800.jpg 800w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-1147.jpg 1147w" sizes="600px" width="600" height="1150" alt="A monstera plant on a windowsill overlooking a city skyline"/></a></figure><figure><a href="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-1147.jpg"><img src="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-800.jpg" srcset="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-200.jpg 200w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-400.jpg 400w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-800.jpg 800w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera_untensils-1147.jpg 1147w" sizes="600px" width="600" height="1146" alt="A Pyrex measuring cup on a stovetop"/></a></figure>
<p>I’m traveling soon, so I had my Hermes agent set up a service to email my roommates when my plants need to be watered. Each plant has its own personality and requests watering in its unique voice, including the amount of water it wants, which measuring utensil to use, and a photo of the plant. Most of my plants need to be watered on a once-per-week cadence, so the service collapses them into a single digest when multiple plants need watering on the same day.</p>
<figure><a href="https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-1147.jpg"><img src="https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-400.jpg" srcset="https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-200.jpg 200w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-400.jpg 400w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-800.jpg 800w, https://jackkrone.com/images/letting-my-plants-email-my-roommates/calathea-1147.jpg 1147w" sizes="240px" width="240" height="455" alt="A calathea prayer plant in a blue-and-cream ceramic pot"/></a></figure>

<style>
@media (max-width: 767px) {
  [style*=&quot;max-width: 240px&quot;] {
    max-width: none !important;
  }
}
</style>
<p>Before I handed the work to Hermes, I used Claude Code to spec out the idea and create the initial prompt. Hermes built the service from there. It sends emails through its dedicated Gmail address, and when my roommates respond with what they&#x27;ve watered, it updates accordingly. For example, you can say &quot;already watered everything yesterday except the calathea and the laurel fig,&quot; and it&#x27;ll acknowledge your message, send a reminder in a couple of days for the two you missed, and reset the clock for everything else as of yesterday. Hermes also tries to keep as many plants as possible on the same watering day. If most plants get watered the same day but a couple of them get watered a few days late, the service will request an early watering for those plants the following week, on the same day as the rest of the plants. If a specific plant isn&#x27;t very moisture-tolerant, the service will skip a watering for that plant until it resyncs with the main once-a-week schedule.</p>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/letting-my-plants-email-my-roommates/monstera-social-top.jpg" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>Setting up Hermes Agent on an old Mac</title>
      <link>https://jackkrone.com/projects/setting-up-my-hermes-agent/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/setting-up-my-hermes-agent/</guid>
      <pubDate>Thu, 23 Jul 2026 02:30:00 GMT</pubDate>
      <description>Reviving a decade-old MacBook Pro with Linux to run a personal AI agent I can message from anywhere.</description>
      <content:encoded><![CDATA[<figure><a href="https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-1920.jpg"><img src="https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-800.jpg" srcset="https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-200.jpg 200w, https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-400.jpg 400w, https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-800.jpg 800w, https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-1600.jpg 1600w, https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-1920.jpg 1920w" sizes="600px" width="600" height="450" alt="An old MacBook Pro running Linux, held in one hand, showing the Hermes Agent splash screen in a terminal, with lilies and a fern behind it"/></a></figure>
<p>I recently set up a Hermes Agent because I wanted to see what the hype was all about with personal AI agents. In a rare case where keeping old electronics worked out in my favor, I used my old college-era MacBook Pro. I must have been doing something weird with it when I last used it because there was no OS installed on the machine. For some reason, booting the Mac in recovery mode only allowed me to download macOS Mavericks, the OS the machine shipped with when it was made in 2014. From there, there was no option to update to a newer OS, despite the fact that the machine ran on Catalina when I abandoned it. I ended up downloading Ubuntu onto a USB stick from another device and installing the OS from the stick.</p>
<p>It took a bit more finagling to get the machine usable. The Linux keyboard shortcuts felt unnatural to me, so I found <a href="https://github.com/RedBearAK/toshy" target="_blank" rel="noopener noreferrer">Toshy</a>, a package that maps macOS keyboard shortcuts onto Linux. That made zooming around between terminal tabs a lot easier. The MacBook also had some weird network and heat issues at first while running Linux. Wi-Fi speeds were incredibly slow, so I had to install a new Wi-Fi driver, and I used Codex to write and test some custom fan logic so the fans would ramp up sooner in response to heavy CPU loads to keep the machine from overheating.</p>
<p>To get Hermes on equal footing with my other machines, I had it clone my <a href="https://jackkrone.com/projects/setting-up-a-cloud-dev-env" target="_blank" rel="noopener noreferrer">cloud dev env repo</a> and run all the same setup steps that project does when provisioning a new VPS. That gave the Hermes machine all the basic tooling I have everywhere else without me having to configure anything by hand.</p>
<p>So far I haven&#x27;t used Hermes for much, but I did set it up with the LLM-memory system Honcho (by Plastic Labs), and it was cool to just message Hermes on Telegram and tell it to figure out how to set up Honcho. I&#x27;ve also been helping a friend set up a personal website, and when they sent me an email with some updated copy, I had a pretty cool moment just forwarding the email to Hermes and telling it to update the site accordingly.</p>
<br/>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/setting-up-my-hermes-agent/hermes-agent-social.jpg" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>Setting up a reproducible cloud development environment</title>
      <link>https://jackkrone.com/projects/setting-up-a-cloud-dev-env/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/setting-up-a-cloud-dev-env/</guid>
      <pubDate>Fri, 10 Jul 2026 04:00:00 GMT</pubDate>
      <description>Standing up a reproducible, disposable VPS with one cloud-init script to run coding agents in YOLO mode.</description>
      <content:encoded><![CDATA[<p>I recently set up a reproducible cloud development environment: one <code>cloud-init</code> script that stands up a VPS running Ubuntu 24. The point of the box is to run coding agents. Claude Code, Codex, and OpenCode are provisioned in full-permission YOLO mode, with a machine-wide policy that blocks force-pushing to and deleting remote <code>main</code> in every repo on the VPS. Credentials live in a git-ignored <code>.env</code>. The <code>cloud-init</code> script sets up a DigitalOcean Droplet, but it can be swapped out for a VPS on any other cloud. The script installs all the packages and tools I&#x27;ll need, like <code>git</code>, <code>tmux</code>, <code>nvim</code>, <code>uv</code>, and <code>node</code>, as well as a number of helpful TUI tools I&#x27;ve been using lately, such as <code>yazi</code> and <code>lazygit</code>. It also sets <code>zsh</code> as the default shell with the same <code>.zshrc</code> file I use locally.</p>
<p>Once GitHub auth completes (for the <code>gh</code> CLI), the VPS clones an explicit allowlist of my repos into the same grouped-parent worktree layout I use locally: a primary checkout plus a linked worktree for each unmerged remote branch. There&#x27;s a <code>land-task</code> script available to agents as the single sanctioned path for merging PRs, and a <code>prune-landed</code> script removes worktrees and local branches once their PRs land.</p>
<p>I was initially motivated to set up this cloud dev environment for basic security reasons. I wanted to run my coding agents in YOLO mode and not think twice. If something goes wrong on the VPS, I can nuke it and replace it, or move it to a different cloud provider. I&#x27;ve mostly been using it for convenience though, SSH&#x27;ing in from my phone (using the Termius app) so I can work on the go.</p>
<p>One of the major trends right now in tech is that software is becoming highly bespoke, especially for the individuals using coding agents the most. But I&#x27;m not so sure that this trend will continue with cloud development environments. They will undoubtedly grow like crazy in the coming months, but their growth might have more to do with local hardware constraints than security concerns. If my main coding agent thread wants to spin up 100 subagents, I could hit the constraints of my device really quickly. But if the main thread is on a VPS that can spin up subagents in other VPSs, then the problem goes away. I think it&#x27;s fairly likely that someone will figure out a way to productize this workflow in a way that&#x27;s good enough for most developers. And many software businesses may be willing to pay for that product.</p>
<br/>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/setting-up-a-cloud-dev-env/featured-social.png" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>The last abstraction: cutting frameworks and dependencies with coding agents</title>
      <link>https://jackkrone.com/projects/the-last-abstraction/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/the-last-abstraction/</guid>
      <pubDate>Tue, 26 May 2026 23:50:00 GMT</pubDate>
      <description>How coding agents replaced framework abstractions in this site&apos;s stack.</description>
      <content:encoded><![CDATA[<p>This site is intentionally over-engineered. I built it to learn a handful of web concepts and tools firsthand. I wrote about that initial journey <a href="https://jackkrone.com/projects/this-site" target="_blank" rel="noopener noreferrer">here</a>.</p>
<p>Shortly after launching this site, I grew frustrated with the framework I used to build it: Gatsby. The team behind Gatsby released v4 weeks after I finished the site, and migrating from v3 didn’t look simple. When I built the site, Gatsby and Next.js were the obvious choices for a pre-rendered React static site with blog posts in Markdown. Next.js also had SSR<sup>[1]</sup>, and according to NPM Trends, it had roughly four times the daily downloads of Gatsby. I didn’t view that as a decisive difference; since Gatsby felt slightly more geared toward pure static sites, I went with it. You can see the rest in the chart below. By today&#x27;s numbers, Next.js is on another planet. That matters because <a href="https://gwern.net/holy-war#bitrot" target="_blank" rel="noopener noreferrer">code that gets used gets maintained</a>. Gatsby&#x27;s plugins eventually drifted out of sync with each other and with the core. Next.js was the better bet.</p>
<figure><a href="https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-1920.png"><img src="https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-800.png" srcset="https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-200.png 200w, https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-400.png 400w, https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-800.png 800w, https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-1600.png 1600w, https://jackkrone.com/images/the-last-abstraction/gatsby-vs-next-1920.png 1920w" sizes="800px" width="800" height="316" alt="NPM download trends: Gatsby vs Next.js"/></a><figcaption>NPM download trends: Gatsby vs Next.js</figcaption></figure>
<p>I kept telling myself I would migrate off Gatsby, or at least upgrade to v4, but I never got around to it. A few weeks ago, I transferred my domain’s DNS management to Cloudflare and decided to move from Netlify to Cloudflare Pages in the process. Cloudflare Pages requires Node.js v22. Gatsby v3 doesn&#x27;t run on it. Claude upgraded the project to v5 (the latest version of Gatsby) and all its dependencies to Node.js v22 compatible versions in about 15 minutes.</p>
<p>Fast forward a couple of weeks, and I read <a href="https://isene.org/2026/05/Audience-of-One.html" target="_blank" rel="noopener noreferrer">A Desktop Made For One</a>, in which the author notes that coding agents have enabled him to build custom software for nearly all of his personal computing needs. That got me thinking. Although I should have initially chosen Next.js over Gatsby for this site, Next.js is by no means the perfect fit for this site. It’s excellent for getting projects off the ground, but it does far more than most projects actually need. I didn’t just migrate my site from Gatsby to Next.js. Instead, I custom-built the specific functionality this site actually uses.</p>
<p>Here&#x27;s what I did, roughly in order:</p>
<ul>
<li>Replaced miscellaneous Gatsby plugins (fonts, analytics, tweet embedding, SEO) with direct implementations.</li>
<li>Replaced Gatsby image plugins with a custom pre-bundle pipeline built on <code>sharp</code> (the library behind the image plugins in Gatsby, Next.js, Vite, and Webpack), plus a custom React component to replace Gatsby&#x27;s lazy-loading image component</li>
<li>Swapped Gatsby&#x27;s internal routing logic with React Router</li>
<li>Migrated off Gatsby&#x27;s GraphQL build-time queries with custom content injection</li>
<li>Replaced Gatsby&#x27;s MDX (Markdown w/ JSX) parsing with a custom script</li>
<li>Decoupled entirely from Gatsby by migrating to Vite, which handles TypeScript transpilation, bundling, and the dev/build server</li>
</ul>
<p>But then I asked myself: why stop there? Sure, Vite is popular, but do I really need it just for its &quot;lightning fast HMR&quot; dev server? If I were building an enterprise application, perhaps. But for a personal blog? No.</p>
<p>So I kept going:</p>
<ul>
<li>I replaced Vite with esbuild for transpilation and bundling, plus custom pipelines for CSS and MDX.</li>
<li>Set up a custom Static Site Generation (SSG) script to run after the bundle (formerly handled by Gatsby)</li>
<li>Set up code-splitting and link prefetching (formerly handled by Gatsby)</li>
<li>Added caching for the image and content pre-bundle steps</li>
<li>Simplified the project&#x27;s linting and formatting config</li>
<li>Ditched Bootstrap in favor of plain CSS</li>
<li>Dropped the <code>react-icons</code> dependency and inlined the handful of icon SVGs the site needs.</li>
</ul>
<p>Here&#x27;s what my dependencies looked like before and after this project:<sup>[2]</sup></p>
<figure><a href="https://jackkrone.com/images/the-last-abstraction/dependencies-before-886.png"><img src="https://jackkrone.com/images/the-last-abstraction/dependencies-before-800.png" srcset="https://jackkrone.com/images/the-last-abstraction/dependencies-before-200.png 200w, https://jackkrone.com/images/the-last-abstraction/dependencies-before-400.png 400w, https://jackkrone.com/images/the-last-abstraction/dependencies-before-800.png 800w, https://jackkrone.com/images/the-last-abstraction/dependencies-before-886.png 886w" sizes="800px" width="800" height="1441" alt="Dependencies before the project"/></a><figcaption>Before</figcaption></figure><figure><a href="https://jackkrone.com/images/the-last-abstraction/dependencies-after-774.png"><img src="https://jackkrone.com/images/the-last-abstraction/dependencies-after-774.png" srcset="https://jackkrone.com/images/the-last-abstraction/dependencies-after-200.png 200w, https://jackkrone.com/images/the-last-abstraction/dependencies-after-400.png 400w, https://jackkrone.com/images/the-last-abstraction/dependencies-after-774.png 774w" sizes="774px" width="774" height="1084" alt="Dependencies after the project"/></a><figcaption>After</figcaption></figure>
<p>And here&#x27;s what my scripts look like:</p>
<figure><a href="https://jackkrone.com/images/the-last-abstraction/scripts-before-1304.png"><img src="https://jackkrone.com/images/the-last-abstraction/scripts-before-800.png" srcset="https://jackkrone.com/images/the-last-abstraction/scripts-before-200.png 200w, https://jackkrone.com/images/the-last-abstraction/scripts-before-400.png 400w, https://jackkrone.com/images/the-last-abstraction/scripts-before-800.png 800w, https://jackkrone.com/images/the-last-abstraction/scripts-before-1304.png 1304w" sizes="800px" width="800" height="271" alt="Scripts before the project"/></a><figcaption>Before</figcaption></figure><figure><a href="https://jackkrone.com/images/the-last-abstraction/scripts-after-1368.png"><img src="https://jackkrone.com/images/the-last-abstraction/scripts-after-800.png" srcset="https://jackkrone.com/images/the-last-abstraction/scripts-after-200.png 200w, https://jackkrone.com/images/the-last-abstraction/scripts-after-400.png 400w, https://jackkrone.com/images/the-last-abstraction/scripts-after-800.png 800w, https://jackkrone.com/images/the-last-abstraction/scripts-after-1368.png 1368w" sizes="800px" width="800" height="239" alt="Scripts after the project"/></a><figcaption>After</figcaption></figure>
<p>The net result is that I have more ownership over my site. Literally, it depends on less code that I don&#x27;t own.</p>
<p>This project reminded me that coding agents are the ultimate abstraction. You no longer write the code; you simply define what you want the code to be. In the past, we reached for all kinds of abstractions because they reduced the cognitive load required to ship features. Now, we have coding agents. They render many other abstractions obsolete. The path of least resistance is no longer a thick wrapper around React. It&#x27;s a spec — and an agent that implements it.</p>
<p>This project barely scratches what coding agents can do. Yet, it may be a preview of what is to come: more custom-tailored code that&#x27;s resilient to the drift of dependencies that reinvent themselves or go unmaintained. On the other hand, none of us know what the future holds. In coming months, we may find that this kind of work is merely tinkering. Junior engineers and non-technical builders are having great success shipping fast with coding agents. Unencumbered by mental models of how things <em>ought</em> to be, they simply trust the LLM. That may matter more than the stack.</p>
<br/>
<hr/>
<p><strong>Notes:</strong></p>
<ol>
<li id="fn-1">
<p>Gatsby supports SSR now as well, since v4.</p>
</li>
<li id="fn-2">
<p>The one thing I dropped in the process is the site&#x27;s PWA setup for offline reading, formerly handled by <code>gatsby-plugin-offline</code>.</p>
</li>
</ol>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/the-last-abstraction/command-line-icon-social-top.png" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>Mortgage recycling calculator</title>
      <link>https://jackkrone.com/projects/mortgage-recycling-calculator/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/mortgage-recycling-calculator/</guid>
      <pubDate>Sun, 03 May 2026 16:47:00 GMT</pubDate>
      <description>A calculator to estimate tax savings from converting non-deductible mortgage debt into deductible investment debt.</description>
      <content:encoded><![CDATA[<p>Debt recycling is a tax strategy that converts a loan with non-deductible interest into a loan with deductible interest. It&#x27;s most commonly used with mortgages. At the federal level, interest on the first $750k of a mortgage is deductible. The same is true of a home equity lines of credit (HELOC), as long as the loan is used to buy, build, or substantially improve your home. The $750k cap applies to mortgages on your primary or secondary residences. So, if you have a $500k mortgage on your first home and a $250k mortgage on your second home, the interest on both is fully deductible.</p>
<p>For very wealthy buyers, the $750k cap is a problem. If you want to buy a $10M house with a $2M down payment and an $8M mortgage, interest on less than 10% of the loan is deductible. But, for cash-rich buyers, there&#x27;s an easy way to recycle this debt into a fully deductible form, even for your third, fourth, or fifth home. First, use delayed financing, meaning you buy your home in cash, then mortgage it. Take the funds from the mortgage and invest them in equities (or other securities), and now, for tax purposes, the full value of the mortgage is considered a margin loan. Interest on margin loans is fully tax deductible.</p>
<p>Voilà. You have successfully recycled your debt. The same strategy can be used when doing a cash-out refinance. If you invest the cash, then interest on the new mortgage is fully deductible, no matter the size of the loan.</p>
<p>I built a simple tool <a href="https://mortgagerecycling.com" target="_blank" rel="noopener noreferrer">here</a> that shows how much you stand to save over time by debt recycling your mortgage. At the moment, it&#x27;s just a static site. If it gains some traction I&#x27;ll add accounts, with the ability to save and share plans.</p>
<br/>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/mortgage-recycling-calculator/recycle-symbol-social-center-top.png" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>Adapting automated real estate appraisals to new markets</title>
      <link>https://jackkrone.com/projects/streamlining-real-estate-appraisals/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/streamlining-real-estate-appraisals/</guid>
      <pubDate>Sat, 03 Sep 2022 17:30:00 GMT</pubDate>
      <description>Making real estate appraisal software flexible for new property types and regions</description>
      <content:encoded><![CDATA[<p>I currently work at a tech-enabled real estate startup called Bowery Valuation.
The company operates as a high-margin appraisal firm, enabled by an in-house web app that its appraisers use to automate and organize the appraisal process.
It&#x27;s like turbo-tax for real estate appraisals, with pages of interconnected forms that guide the appraiser through the appraisal process while computing the valuation of a property and generating the text of the appraisal report.
The company&#x27;s bread-and-butter revenue source is mixed-use multifamily buildings in the New York metro.</p>
<p>Because most mixed-use multifamily sales are valued in a similar fashion to one another, Bowery&#x27;s web app has a good deal of automation built in for generating these reports, and its appraisers can complete appraisals on these properties more efficiently than its competitors, leading to higher operating margins.
But just as turbo-tax loses its edge as an individual&#x27;s taxes become more complicated, Bowery&#x27;s appraisers are less efficient when using the web app to appraise different types of properties.
The same problem exists for properties outside of the New York metro, where local business practices and state regulations require different types of information in a given appraisal.
Some properties, such as marinas and golf courses, are appraised infrequently enough that there is little value in creating new features to speed up the appraisal process for these specific property types within Bowery&#x27;s web app.
On the other hand, there are key growth opportunities in efficiently appraising common property types such as warehouses and office buildings.
Furthermore, we want to enable appraisers across the US to appraise properties on the app as easily and efficiently as appraisers in our home market of New York.</p>
<p>To lay the groundwork for this business objective, I recently lead a project to make Bowery&#x27;s web app more flexible for different property types and new geographies.
The work addressed the fundamental unit of an appraisal: square footage.
As it turns out, different properties types and different regions of the US call for different methods of measuring square footage in an appraisal.
The standard way to measure it in a mixed-use multifamily is with gross building area (GBA), which is the sum of the footprints of each story of the building.
The footprint is calculated from the exterior walls of the building, so GBA is strictly greater than the inhabitable interior area of any given building.
Other common metrics used in appraisals include gross leasable area (GLA), net rentable area (NRA), and net leasable area (NLA).</p>
<p>This project had 3 requirements:</p>
<ol>
<li>enable appraisers to select from a list of square footage metrics to base their appraisal on, specifically GBA, GLA, NRA, and NLA, while maintaining GBA as the default option</li>
<li>update computed values throughout the appraisal that depend on the value of the square footage</li>
<li>update generated text in the appraisal report to reflect the square footage metric being used</li>
</ol>
<p>Before jumping into the details of how I created this feature, let me give a broad overview of what the system architecture looks like.
We have a monolithic app with four different teams committing changes to it.
The front end is written in javascript (using react) and is a series of interrelated forms organized into different sections that map roughly to the sections of an appraisal report.
There&#x27;s one section for basic property information, such as the address and square footage, and another section for recording the occupancy status, rent, and additional information about the building&#x27;s units (if applicable).
There&#x27;s also a section for quickly identifying comparable sales, another for building a valuation based on those comps, and another for building a valuation based on the projected cash flow of the property.
The frontend talks to a NodeJS backend with an Express API (javascript), and we&#x27;re in the process of migrating it to NestJS (typescript).
We use a MongoDB database, and our ODM models (mongoose) are fully migrated to NestJS, which means they have the added type-safety that comes with typescript.
The backend also talks with various public APIs to automatically collect property information from state- and city-run data sources like New York City&#x27;s PLUTO (Primary Land Use Tax Lot Output).</p>
<p>The forms the appraiser fills out in the app produce a MongoDB document associated with that appraisal.
The document is continually updated and returned to the frontend as the appraiser saves their progress, where its value is held in the client&#x27;s state using redux. It looks something like this:</p>
<pre><code>{
	_id: object id
	report_generated: timestamp
	report_updated: timestamp
	appraiserId: int
	clientId: int
	latitude: double (i.e. floating-point number)
	longitude: double
	street1: string
	street2: string
	city: string
	state: string
	zip: int
	...
	...
	...
	GBA: int
	...
	...
	...
}
</code></pre>
<p>As you can imagine, there&#x27;s much more information contained in an appraisal than I&#x27;m showing here, including many nested documents containing details about the property and the process through which multiple valuations of the property are determined, as well as custom text the appraiser wants to include in their appraisal report.
In this example, the GBA (gross building area) lives at the top level of the document object, although in practice it lives in a nested document.
When the appraiser finishes filling out all the required forms in the app, they&#x27;ll see a button prompting them to generate a report.
This sends the appraisal document to an appraisal-generation service written in C# that outputs a formatted word document as the (nearly) final deliverable for the appraisal client.</p>
<p>Alright, so now that we have the architecture out of the way, let&#x27;s jump into the details of how we implemented this feature.
The first challenge was deciding how to store different square footage metrics in the mongoDB document, keeping in mind our intention to design the feature to easily accommodate additional square footage metrics in the future.</p>
<p>The final changes looked something like this:</p>
<pre><code>{
	id: object id
	report_generated: timestamp
	report_updated: timestamp
	appraiserId: int
	clientId: int
	latitude: double
	longitude: double
	street1: string
	street2: string
	city: string
	state: string
	zip: int
	...
	...
	...
	GBA: int
	squareFootage: int
	squareFootageMetric: string (default: &#x27;GBA&#x27;)
	...
	...
	...
}
</code></pre>
<p>We added two new fields: the integer <code>squareFootage</code> and the string <code>squareFootageMetric</code>.
You might be wondering why we didn&#x27;t remove the GBA field while we were at it.
The reason for this is that there are two calculations that rely on GBA in every appraisal, even if the valuation conclusions are based on some other square footage metric.
These two values are peripheral to the main conclusions of an appraisal and the details behind them are beyond the scope of this post.
That said, they added complexity to the project because they required the app&#x27;s forms to always collect GBA, and to optionally collect either GLA, NRA, or NLA.
Fortunately, having the GBA field present actually helped my team implement the code changes in a piecemeal fashion that avoided merge conflicts in areas of the code that other teams were working on.</p>
<p>As I mentioned earlier, our database models are already migrated to NestJS, which means they have the type safety provided by typescript.
As a result, we could constrain the field <code>squareFootageMetric</code> to the following subset of the type <code>string</code>:</p>
<pre><code>enum squareFootageMetrics {
	GBA = &#x27;GBA&#x27;
	GLA = &#x27;GLA&#x27;
	NRA = &#x27;NRA&#x27;
	NLA = &#x27;NLA&#x27;
};
</code></pre>
<p>In practice, we didn&#x27;t actually use an enum.
It&#x27;s a typescript feature that I avoid.
Much has been written on the topic, so I won&#x27;t dive into the details here.
Instead of using an enum, we constrained <code>squareFootageMetric</code> with a dynamic type generated from an object.
The general consensus in the typescript community for working around the use of enums is as follows:</p>
<pre><code>const squareFootageMetrics = {
	GBA: &#x27;GBA&#x27;,
	GLA: &#x27;GLA&#x27;,
	NRA: &#x27;NRA&#x27;,
	NLA: &#x27;NLA&#x27;,
} as const;

// the following generates type squareFootageMetricsType = &#x27;GBA&#x27; | &#x27;GLA&#x27; | &#x27;NRA&#x27; | &#x27;NLA&#x27;
type squareFootageMetricsType = typeof squareFootageMetrics[keyof typeof squareFootageMetrics];
</code></pre>
<p>Now that we had updated the data structure, I created a data migration to adjust existing records.
It took every existing appraisal document, set <code>appraisal.squareFootage</code> to the value of the <code>appraisal.GBA</code> field and set <code>appraisal.squareFootageMetric</code> to <code>&#x27;GBA&#x27;</code>.
With the data migrated, the next step was project requirement #1: to enable appraisers to select from a list of square footage metrics to base their appraisal on.
The existing app included a form field like the following:</p>
<p>GBA: <input/></p>
<p>I created a list of radio buttons above this input asking the user to select the square footage metric they want to use for their appraisal.
The default selection for the list is GBA, and the options are created dynamically from the array <code>Object.values(squareFootageMetrics)</code>.
Since <code>squareFootageMetricsType</code> is also created dynamically from <code>squareFootageMetrics</code>, we can easily add additional square footage metrics to the system in the future with a single line of code.
If the user selects a radio button other than GBA, then an additional form field underneath GBA appears.
The GBA input is required, and the second input is required if showing.
It works like this:</p>
<p><a href="https://jackkrone.com/projects/streamlining-real-estate-appraisals/">Try the interactive version on the site.</a></p>
<br/>
<p>When the page is saved, the radio button selection sets the value of <code>appraisal.squareFootageMetric</code>, the first text input sets the value of <code>appraisal.GBA</code>, and the second text input sets the value of <code>appraisal.squareFootage</code>.
If the second text input is null (which occurs when the GBA radio button is selected), then <code>appraisal.squareFootage</code> is set to the same value as the <code>appraisal.GBA</code>.</p>
<p>The last stage of development, where the bulk of the code changes actually occurred, was to change ~30 values downstream of a property&#x27;s square footage and the basis for determining it (GBA, NLA, etc.) in a given appraisal.
My team uses a JIRA Kanban board to track and coordinate our work, so I created an individual ticket on the board for each required downstream change.
Note that I also broke down the aforementioned work into tickets, and I classed all tickets for this project under the same epic.</p>
<p>For the downstream changes, there were 3 types of tickets:</p>
<ol>
<li>changes to the generated text in the outputted .docx appraisal report: these involved changing a hardcoded string such as <code>&#x27;GBA&#x27;</code> to a dynamic template string like <code>`${appraisal.squareFootageMetric}`</code>.</li>
<li>changes to a computed value: these involved changing a piece of frontend or backend code such as:<br/>
<code>const pricePerSquareFoot = appraisal.valuation / appraisal.GBA</code><br/>
to:<br/>
<code>const pricePerSquareFoot = appraisal.valuation / appraisal.squareFootage</code></li>
<li>changes to a computed value and a string value: these involved the process mentioned in #2 and an additional change to some associated frontend string value such as:<br/>
<code>&quot;The unit&#x27;s portion of total building area is unit area divided by GBA&quot;</code><br/>
to:<br/>
<code>`The unit&#x27;s portion of total building area is unit area divided by ${appraisal.squareFootageMetric}`</code></li>
</ol>
<p>As I mentioned earlier, because we didn&#x27;t remove the <code>appraisal.GBA</code> field from the MongoDB document, we were able to implement these changes in a piecemeal fashion.
Each change was isolated from the others, so we made a single branch and pull-request for each ticket.
This made for quick turnaround times on code reviews and also enabled us to prioritize certain changes ahead of others.
In some cases we would make a change to a specific form quickly if we knew another team was about to start working on that form.
In other cases, we could wait a few days to touch a specific form while another team finished up and merged changes to that form.
This naturally gave our team an incentive to review certain pull-requests quickly in order to speed up our own work.
At this point in the course of the project I effectively stopped coding.
My time was spent most effectively by coordinating my team&#x27;s work around the work of other teams and by quickly reviewing the pull-requests my team members were opening.</p>
<p>Overall the process went smoothly. One of the hiccups we ran into along the way is that we found about 10 additional downstream values that needed to be changed.
Our design team found about half of them and our engineers discovered the other half.
I had already given estimates of our timeline before these tickets were added to the epic, but I was careful to communicate that strictly the engineering work (excluding QA, design, and product reviews) would be done for the <em>existing</em> tickets by a specific date
This made it easy to update our self-imposed deadline when additional necessary changes were found.
I just made ~10 new tickets and shared a new deadline for those tickets.
Communicating this way turned out to be one of the most important decisions I made during the process as it prevented my team from getting stuck in a time crunch to hit a deadline.</p>
<p>Ultimately, the implementation of this feature is straightforward and easy to communicate. This is the aspect of the project I&#x27;m most pleased with.
Without a well thought out plan for its architecture and implementation we easily could have ended up with a hacky, complex solution that would need reworking in the future.
Bowery&#x27;s web app now has a simple way for an appraiser to change the basis of a property&#x27;s square footage calculation, and it has a trivially easy way for developers to further increase an appraiser&#x27;s flexibility in this regard.
This directly aids Bowery&#x27;s operational efforts as it expands its business model to new appraisal types and geographies.</p>
<br/>
<hr/>
<p>Special thanks to <a href="https://www.linkedin.com/in/shuvo-rahman-368350108/" target="_blank" rel="noopener noreferrer">Shuvo Rahman</a>, who helped plan the changes to the data structure.</p>
<br/>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/how-i-built-this-site/featured-social.jpg" medium="image" width="1200" height="630" />
    </item>
    <item>
      <title>How I built this site</title>
      <link>https://jackkrone.com/projects/this-site/</link>
      <guid isPermaLink="true">https://jackkrone.com/projects/this-site/</guid>
      <pubDate>Thu, 16 Dec 2021 22:18:15 GMT</pubDate>
      <description>The how and why behind jackkrone.com and lessons learned along the way.</description>
      <content:encoded><![CDATA[<p>This site is a modern, pre-rendered website written in TypeScript. Try some of the following things and see what happens:</p>
<ul>
<li>Navigate around the site, and see if your browser ever indicates that it’s loading</li>
<li>On the home page, switch to dark mode and refresh the page while keeping an eye on the banner and favicon</li>
<li>Turn off your internet connection and see if you can still navigate around the site</li>
<li>Check out the mobile site if you’re on desktop (or vice versa)</li>
</ul>
<p>The site’s features include:</p>
<ul>
<li>Fully responsive design</li>
<li>Lazy-loaded images</li>
<li>Optimized image resolution for different screen sizes</li>
<li>Progressive web app functionality with offline navigation</li>
<li>Light and dark mode</li>
<li>JAMStack setup with GraphQL</li>
<li>Google analytics integration</li>
</ul>
<p>At the time of writing there’s still a few kinks to work out, and I’m sure I&#x27;ll add some features and/or tweak the layout over time, but things are mostly good to go. I built this site with TypeScript, React, and Gatsby, and I deployed it with Netlify. You might ask, &quot;Why use TypeScript for such a simple site?&quot; Well, I had never used TypeScript before and I wanted to learn how it works. I used Bootstrap 5 for styling, and chose not to use it with react-bootstrap. Since Bootstrap 5 no longer uses jQuery, react-bootstrap is partially redundant. Plus, when I started this project, I already had experience using React style components (using Material UI) so I used this project to learn the authentic form of Bootstrap.</p>
<p>I had multiple false starts on this project. For instance, I considered using a Gatsby <a href="https://www.gatsbyjs.com/starters" target="_blank" rel="noopener noreferrer">starter</a> but after beginning development on one, was frustrated to discover it was using v2 of Gatsby. I wanted to be using the latest major release. Attempting to migrate the project from v2 to v3 was a headache, so I started the project over, and this time from zero. Literally, with <code>npm init -y</code>. I had a heck of a time figuring out all the configuration and adding packages one-by-one, but in the end I may have learned more from the setup alone than from the process of building the site. This gives me confidence to work on more complex projects in the future that don’t or can’t rely on common boilerplate like <code>create-react-app</code>. While getting everything set up, I learned to use a number of useful packages and tools including <code>nvm</code>, <code>react-helmet</code>, and <code>bootstrap-dark-5</code> (enables bootstrap with dark mode). I also learned how to persuade ESLint and Prettier to cooperate with eachother with the help of <code>eslint-plugin-prettier</code>.</p>
<p>Before I continue, it&#x27;s worth mentioning that this site’s core functionality is blogging. As noted in the site features list, I used a JAMStack setup. This makes creating new posts extremely easy. Making a new post is as simple as dropping a new markdown file in the appropriate directory and adding a few lines of frontmatter at the top of the file. Frontmatter is just data associated with the post such as the title, date, and slug (URL endpoint). Frontmatter is formatted as a YAML object and placed before any markdown content in the file. It&#x27;s useful because Gatsby can query it with GraphQL and then use it to programmatically create content on your site <em>at build time</em>, including stylized links to posts and the posts themselves. This is a key point to keep in mind. Gatsby is a static-site-generator, meaning that the site contents are generated at build time. Gatsby doesn’t allow you to use GraphQL queries during runtime.</p>
<p>A cool feature of this site is that it uses MDX, which enables the use of JSX inside markdown files. With MDX, you can import react components at the top of a markdown file and use them later on in the file. I created a custom <code>MdxImage</code> component to handle images in markdown files. There are a few Gatsby plugins (all forked from <code>gatsby-remark-images</code>) that simplify using screen-size-optimized images in markdown files with markdown formatting, but none let you define a max width for individual images. By default these plugins make every image take up the entire width of the container they’re in. That bothered me, so I created a custom react component. In the future, I may write a post specifically about the <code>MdxImage</code> component since I know I’m not the only one frustrated with the lack of functionality from <code>gatsby-remark-images</code>.</p>
<p>Other than the JAMstack setup, I got into the weeds on a number of topics while working on this project and was dogmatic about some things (e.g. preferring margin over padding) to ensure future site maintenance is as straightforward as possible. I realized some interesting things while in the weeds, including that using an svg favicon with built-in dark mode is difficult if not impossible on Safari. If you don&#x27;t know what I&#x27;m saying, take a look at Github&#x27;s dark mode favicon on Safari versus its dark mode favicon on Chrome. On Chrome, the favicon changes color in dark mode. On Safari, the favicon just gets a white background in dark mode.</p>
<p>Another interesting thing I realized is that TypeScript has a difficult time working with the <code>Array.prototype.reduce()</code> method when it&#x27;s passed an empty object as its initial value. I realized this while creating the display for the Thoughts page and the Projects page. On both pages, the headers are numeric years and each header sits above a list of posts from that given year. The display will only render headers for years in which there were posts. So, for example, if for some reason I only had posts on my site from 2021 and 2005, those would be the only two headers and there would be no intermediary headers for 2006-2020. To create this display, I intially utilized the <code>Array.prototype.reduce()</code> method. This approach was unnecessarily complicated and I ended up changing it later on, but the lesson I learned in the process is worth mentioning.</p>
<p>My approach started with a GraphQL query to get all the posts for either the Thoughts or the Projects page. The query would return an array of objects where each object included a post title, date, and slug. From here, I wanted to break this array into sub-arrays, one for each year in which there were posts, and then store the sub-arrays in an object. In case that&#x27;s confusing, I&#x27;ll state it another way. I took the return value of the GraphQL query and I used <code>reduce()</code> to loop through the array of objects while creating a new object with the following structure:</p>
<pre><code>{
  2021: [
    { title: post1title2021, date: post1date2021, slug: post1slug2021 },
    { title: post2title2021, date: post2date2021, slug: post2slug2021 },
    … ],
  2005: [
    { title: post1title2005, date: post1date2005, slug: post1slug2005 },
    … ],
  …
}
</code></pre>
<p>With this object in hand, it&#x27;s easy to render headers for only the years in which there are posts. For a simplified version of how I used <code>reduce()</code> to create this object from an array, check out the answers to <a href="https://stackoverflow.com/questions/43008212/reduce-array-of-objects-into-object" target="_blank" rel="noopener noreferrer">this</a> question on Stack Overflow. The approach works by passing the method an empty object as an initial value. The <code>reduce()</code> method then makes iterative changes to the empty object, ultimately returning a non-empty object. This means that the object has a type of either <code>someObjectShape | {}</code> or <code>Partial&lt;someObjectShape&gt;</code>.</p>
<p>In either case, this causes TypeScript errors. TypeScript looks at the return value of the <code>reduce()</code> method and notes that it could be the empty object <code>{}</code>, despite the fact that I know it can&#x27;t be an empty object if the aforementioned GraphQL query is set up correctly. Given that the object could be empty, TypeScript determines that any values that depend on the properties of the object could therefore be undefined. This is the root of the problem. TypeScript won&#x27;t accept the possibility of an undefined (or null) value unless it is explicitly stated as a possibility in a variable&#x27;s type declaration. This is TypeScript&#x27;s <code>strictNullChecks</code> rule, one of the key rules in TypeScript&#x27;s strict mode.</p>
<p>As far as I know, there’s no way to fix this issue<sup>[1]</sup>. You could turn off <code>strictNullChecks</code> or, for every variable that depends on the object&#x27;s properties, use optional chaining or append <code>|undefined</code> to the variable&#x27;s type declaration. None of these options are acceptable solutions though as they negate the benefits of using TypeScript strict mode in the first place. My advice is to simply avoid using <code>Array.prototype.reduce()</code> with an empty object as an initial value. And for that matter, avoid using an empty array as an initial value as well.</p>
<p>So, while I ultimately decided against using the approach discussed above, getting to the bottom of the issue taught me a bit about how TypeScript works. This process of learning by doing is exactly what I set out to do when I began this project. Building this site was an enjoyable challenge, and I hoped to fill it with original thoughts over time.</p>
<br/>
<hr/>
<p><strong>Notes:</strong></p>
<ol>
<li id="fn-1">In hindsight (as of 2022), there is a very clear way to fix this issue. The cleanest solution is to type <code>reduce()</code>&#x27;s output with a generic, so the compiler knows the shape of the accumulator from the start. See <a href="https://stackoverflow.com/questions/43008212/reduce-array-of-objects-into-object" target="_blank" rel="noopener noreferrer">here</a>. If you&#x27;d rather, you can also reach for a type assertion, the developer&#x27;s way of telling the TypeScript compiler &quot;You think the type is x, but treat it as y, because I have special information you do not that guarantees this type will always be y, never x.&quot; Assertions should generally be avoided though, since they silence the compiler rather than informing it, and they&#x27;re often used where a plain type declaration would do. So, contrary to my advice above: the reduce function is awesome, I use it often, and I recommend it even when the initial value is an empty object or array.</li>
</ol>
<br/>
<hr/>
<p>Credits: The inspiration for this site comes from <a href="https://training.kalzumeus.com/newsletters/archive/do-not-end-the-week-with-nothing" target="_blank" rel="noopener noreferrer">this</a> essay by Patrick McKenzie.</p>]]></content:encoded>
      <media:content url="https://jackkrone.com/images/home-image-day-social.jpg" medium="image" width="1200" height="630" />
    </item>
  </channel>
</rss>
