<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[On Matters Concerning my Existence]]></title><description><![CDATA[Matters concerning my existence.]]></description><link>https://www.onmattersconcerningmyexistence.com</link><image><url>https://www.onmattersconcerningmyexistence.com/img/substack.png</url><title>On Matters Concerning my Existence</title><link>https://www.onmattersconcerningmyexistence.com</link></image><generator>Substack</generator><lastBuildDate>Fri, 01 May 2026 02:07:26 GMT</lastBuildDate><atom:link href="https://www.onmattersconcerningmyexistence.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Devin]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[onmattersconcerningmyexistence@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[onmattersconcerningmyexistence@substack.com]]></itunes:email><itunes:name><![CDATA[Devin Sit]]></itunes:name></itunes:owner><itunes:author><![CDATA[Devin Sit]]></itunes:author><googleplay:owner><![CDATA[onmattersconcerningmyexistence@substack.com]]></googleplay:owner><googleplay:email><![CDATA[onmattersconcerningmyexistence@substack.com]]></googleplay:email><googleplay:author><![CDATA[Devin Sit]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[I rewrote the easy-window-switcher to Rust]]></title><description><![CDATA["Where's that darn borrow checker when I need it..."]]></description><link>https://www.onmattersconcerningmyexistence.com/p/i-rewrote-the-easy-window-switcher</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/i-rewrote-the-easy-window-switcher</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Tue, 12 Aug 2025 00:15:45 GMT</pubDate><content:encoded><![CDATA[<p>Today, on matters concerning my existence...</p><p>An 8 month delayed update! </p><h1>Introducing: easy-window-switcher-rs! (it's like easy-window-switcher but in RUST)</h1><p>Yes, after writing <a href="https://onmattersconcerningmyexistence.substack.com/p/rewriting-the-easy-window-switcher">the last post</a> during my Christmas break, I did end up rewriting my <a href="https://github.com/DevinSit/easy-window-switcher">easy-window-switcher</a> from Python to Rust. I'm just only now bothering to write the follow up post to announce that it was rewritten: <a href="https://github.com/DevinSit/easy-window-switcher-rs">easy-window-switcher-rs!</a> (I know, very creative name)</p><h1>How did it turn out?</h1><p>Great! Not only is it no longer written in the worst-language-on-Earth (Python), it's written in the greatest-language-on-Earth (Rust)!</p><p>In all seriousness, as much as I never really complained that the Python version was noticeably slow... I must say, the Rust version is like ever so slightly noticeably faster. It just <em>feels</em> instantaneous to invoke and switch between windows, so that's a nice usability win.</p><p>Oh yeah, looking at my old notes, I even did some timing tests between the two versions:</p><pre><code>$ (start=$(date +%s%3N); easywindowswitcher direction left; end=$(date +%s%3N); echo "Elapsed time: $((end - start)) ms")
Elapsed time: 162 ms

$ (start=$(date +%s%3N); easy-window-switcher-rs direction left; end=$(date +%s%3N); echo "Elapsed time: $((end - start)) ms")
Elapsed time: 30 ms

$ (start=$(date +%s%3N); easywindowswitcher monitor 0; end=$(date +%s%3N); echo "Elapsed time: $((end - start)) ms")
Elapsed time: 165 ms

$ (start=$(date +%s%3N); easy-window-switcher-rs monitor 0; end=$(date +%s%3N); echo "Elapsed time: $((end - start)) ms")
Elapsed time: 22 ms</code></pre><p>The Rust rewrite (in its most naive implementation) is about 5 times faster, and I suspect most of the actual execution time is just the time we spend calling out to the other CLI tools, so that's neat.</p><p>On top of that, one of my original goals was to improve the configurability of the tool since the monitor layout was hard-coded into the source code before. Well now, not only is it "more configurable", it's <em>automatic. </em>I went above and beyond centralizing the config to just outright making it detect and work with the computer's actual monitor configuration! (thanks to adding <code>xrandr</code> as another dependency)</p><p>That being said, I didn't exactly rigorously test it with different monitor setups, but it works for mine and that's good enough for me!</p><h1>Final Thoughts</h1><p>Was this rewrite worth it just as a time waster? Yes!<br>Is the new version better in every way? Absolutely!<br>Mission? Success!</p><p>Rust really is quite nice for writing CLI tools. I should do this again some day.</p><p>But until then... </p><p>You should really go learn some Elixir :)</p>]]></content:encoded></item><item><title><![CDATA[Rewriting the easy-window-switcher]]></title><description><![CDATA["Rust all the things"]]></description><link>https://www.onmattersconcerningmyexistence.com/p/rewriting-the-easy-window-switcher</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/rewriting-the-easy-window-switcher</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Fri, 13 Dec 2024 23:00:41 GMT</pubDate><content:encoded><![CDATA[<p>Today, on matters concerning my existence...</p><p>Yes, I'm back. Been a couple years, but for absolutely no reason whatsoever I felt like doing a little bit of public blogging today.</p><p>Today's topic is what I hope to be a fairly quick project that I want to hack on: rewriting the <a href="https://github.com/DevinSit/easy-window-switcher">easy-window-switcher</a>.</p><h1>... what is the easy-window-switcher?</h1><p>Well, if you're too lazy to go read the <a href="https://github.com/DevinSit/easy-window-switcher">README</a>, it's basically a small Python script that wraps around some CLI tools to allow me to more easily switch focus between windows (go figure) in Ubuntu. </p><p>I've got four monitors and alt-tab just doesn't cut it. So, instead, this nifty little script allows me to switch window focus either relatively (i.e. to the closest leftmost or rightmost window relative to the currently-focused window) or absolutely (i.e. to the window on "monitor 1", "monitor 2", etc) by binding some keyboard shortcuts to the tool's commands.</p><p>I'm sure there's gotta be other tools that already do this, but my original motivation boiled down to "well, I know <code>wmctrl</code> can be used to focus windows, so surely I could write a small wrapper to do what I want", and sure enough I could.</p><h1>... so why rewrite it?</h1><p>Just cause I think I can &#175;\_(&#12484;)_/&#175;</p><p>But also because I want to specifically rewrite it in Rust (yes, I've become one of <em>those</em> people) and get some more Rust experience under my belt.</p><p>I've been playing with Rust for the last year or so and have come to quite enjoy it (once I got past the initial learning curve). As I keep saying to those I evangelize it to: "I've never quite enjoyed being yelled at by a computer so much".</p><p>I first started using it <em>slightly</em> out of necessity &#8212; I needed it to do backend Yjs document manipulation in Elixir (via the <a href="https://docs.rs/yrs/latest/yrs/">yrs</a> library) for Knopic (which is a whole other story we're not gonna get into today), but I've recently been just trying to force it where I can for non-web app projects, and rewriting the <code>easy-window-switcher</code> seems like a very natural progression. After all, it's a fairly self-contained program (only depending on some other CLI tools), and it's a program that's in the hotpath of my day-to-day workflow (i.e. speed is nice to have).</p><p>That being said, there is literally nothing wrong with the way I've been using <code>easy-window-switcher</code> so far &#8212; it's not noticeably slow by any means. I just want to write more Rust &#175;\_(&#12484;)_/&#175;</p><p>However, there is one small improvement I hope to make to the program from a "business logic" POV: being able to configure the monitor arrangement more easily. Right now, the monitor configuration (i.e. the number, size, arrangement, and order of monitors) is hard-coded and spread across a couple files in the Python implementation, so I want to consolidate that into one place, as well probably make it configurable via env vars or a config file.</p><p>Oh right, I guess the other 'big' thing we'd get out of this rewrite is making it easier to install the tool &#8212; can just distribute a binary instead of having to <code>pip install</code> it from the repo. Small in the grand scheme of things, but a nice win nonetheless.</p><p>Otherwise, this is just a "light coding project to fill some time". All I've done so far is open an <a href="https://github.com/DevinSit/easy-window-switcher-rs">empty repo</a> and bootstrap a Rust project, along with doing some basic design thinking (copied below), so we'll see where this goes I guess.</p><h1>Design Thoughts</h1><p>We have the following structure the Python implementation:</p><ul><li><p><a href="http://main.py">main.py</a></p><ul><li><p>Main entrypoint that invokes the CLI</p></li></ul></li><li><p>commands/</p><ul><li><p>The CLI commands that invoke the services</p></li></ul></li><li><p>services/</p><ul><li><p><code>window_focuser.py</code> </p><ul><li><p>The primary service that handles all the business logic</p></li></ul></li></ul></li><li><p>external_services/</p><ul><li><p><code>wmctrl.py</code> </p><ul><li><p>A wrapper service around the <code>wmctrl</code> CLI utility for measuring/controlling windows</p></li></ul></li></ul></li><li><p>data_models/</p><ul><li><p><code>window.py</code></p><ul><li><p>A struct for modelling the attributes of a single window</p></li></ul></li><li><p><code>workspace.py</code></p><ul><li><p>A struct for modelling the attributes of a 'workspace' (aka a monitor)</p></li></ul></li><li><p><code>workspace_grid.py</code></p><ul><li><p>A struct for modeeling the attributes of a 'grid of workspaces' (i.e. a single Ubuntu workspace's worth of monitors)</p></li></ul></li></ul></li></ul><p>First thoughts:</p><ul><li><p>Not sure why we call them "workspaces" instead of just "monitors", since "workspaces" means something else entirely in Ubuntu land; should rename them</p></li><li><p>This is quite a simple program and it's actually pretty well structured for something I wrote a long time ago</p><ul><li><p>... I say that, but then I go check the logs and find that I actually only wrote it in 2020...</p></li></ul></li><li><p>Main thing to figure out will be how to handle the calls out to the <code>wmctrl</code> CLI</p><ul><li><p>Also <code>xdotool</code> that I found from inspection (and reading the dependencies list in the README...)</p></li></ul></li></ul><p>Improvements we can make:</p><ul><li><p>Allow the monitor configuration to be more easily configurable</p><ul><li><p>Right now, it's all hard-coded as constants in various files, along with a function that must be modifed to indicate how the monitors are laid out</p></li></ul></li></ul><p>Approach for the rewrite:</p><ul><li><p>Rewrite it more-or-less 1-1 and then refactor once we have it working</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.onmattersconcerningmyexistence.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading On Matters Concerning my Existence! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Open Sourcing uFincs]]></title><description><![CDATA[...but not here]]></description><link>https://www.onmattersconcerningmyexistence.com/p/open-sourcing-ufincs</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/open-sourcing-ufincs</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Tue, 19 Apr 2022 16:45:05 GMT</pubDate><enclosure url="https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/e2a1443e-2298-442b-b41c-969a3fd0c76f_1200x600.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hello there dear reader!</p><p>The day has finally come: uFincs has been open-sourced! Check out the announcement on the <a href="https://blog.ufincs.com/p/open-sourcing-ufincs">official uFincs blog</a> or hop straight on over to the <a href="https://github.com/uFincs/uFincs">repo</a> if you feel so inclined.</p><p>That is all. Carry on.</p>]]></content:encoded></item><item><title><![CDATA[How I (almost) Won a Month-Long Hackathon (in a month)]]></title><description><![CDATA[By Expending Way Too Much Effort]]></description><link>https://www.onmattersconcerningmyexistence.com/p/how-i-almost-won-a-month-long-hackathon</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/how-i-almost-won-a-month-long-hackathon</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Mon, 21 Mar 2022 01:19:54 GMT</pubDate><enclosure url="https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/8ab0a84a-a897-47a5-ac84-d6622c4d72b0_570x463.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Another year, another hackathon. This time, <a href="https://www.onmattersconcerningmyexistence.com/p/how-i-won-a-month-long-hackathon?utm_source=url" title="much like last time">much like last time</a>, I ended up (almost) winning. Except this time, rather than spending only 3 hours to win $2k, I spent over a <em>month</em> to win the same prize. Why? Well, let's just say there was an even bigger prize at stake...</p><p>Enter: the <a href="https://easyserverless.bemyapp.com/" title="GCP &quot;Easy as Pie&quot; Serverless Hackathon">GCP "Easy as Pie" Serverless Hackathon</a>.</p><h1>The Hackathon</h1><p>$20k prize pool. $10k grand prize. 5 "runner-up" $2k prizes. All in the name of trying out the latest and greatest in GCP serverless tech: Cloud Functions, Cloud Run, and Cloud Workflows.</p><p>Unlike the last hackathon &#8212; which had a much more specific theme &#8212; this hackathon's theme was much simpler: just use the serverless tech and build something... Cool. Creative. Interesting. Maybe sprinkle in some machine learning if you're feeling it.</p><p>Well, I was certainly feeling it. These serverless techs were definitely something I had familiarity with (with the exception of Workflows, which was new to me), so this seemed like a great opportunity to flex some tech muscles and put together something a bit more worthy of a $10k grand prize than my last 3 hour "project".</p><p>However, it didn't start out that way. Initially, just like the last hackathon, I wanted to take one of my existing projects &#8212; the <a href="https://github.com/DevinSit/dank-meme-classifier" title="Dank Meme Classifier">Dank Meme Classifier</a> seemed like a perfect fit this time &#8212; deploy it, and call it a day. But as I went over the judging criteria, I decided to think about things some more:</p><p><strong>Technical Execution</strong></p><ul><li><p>How has the team effectively utilized at least one Google Cloud Serverless product &#8212; Cloud Run, Cloud Functions, or Workflows?</p></li><li><p>How easy is the application to use?</p></li><li><p>How advanced is the prototype presented?</p></li></ul><p><strong>Completeness</strong></p><ul><li><p>How soon could your project go to market?</p></li></ul><p><strong>Return on Investment (ROI)</strong>:</p><ul><li><p>Is the project cost-effective?</p></li><li><p>Is the project worth pursuing?</p></li></ul><p>If I wanted to win the grand prize, I'd have to look at maxing out all of these criteria. The Dank Meme Classifier would get close, but I had my concerns about the ROI category: I didn't really think it would be considered a "project worth pursuing". Additionally, while it was a "complete" product, it wasn't a particularly "advanced" one.</p><h1>The Idea</h1><p>As such, I started brainstorming. I still wanted to use the Dank Meme Classifier as a base, but wondered how it could be expanded to be more "advanced"...</p><p>How about a game? What if we <em>gamify</em> classifying memes? </p><p>Yep, that was my brilliant idea: <a href="https://github.com/DevinSit/dank-league-of-memeing-battlegrounds" title="GitHub - DevinSit/dank-league-of-memeing-battlegrounds: The next great eSport for killing time during sprint meetings.">The Dank League of Memeing Battlegrounds</a>. A game where you're given 10 memes, a timer, and the ability to swipe them up and down to guess if they're dank or not. And how do we know if they're <em>actually</em> dank or not? Using the dank meme classifier!</p><p>So how does that idea do on the judging criteria? Well, Technical Execution would be effectively flawless. Cloud Run will host the Frontend (React App) and Backend (Flask API) just like is done for the Dank Meme Classifier, and Cloud Workflows will replace PubSub to coordinate the litany of Cloud Functions that are used for ingesting and processing memes from Reddit. Gotta make sure to use all three services cause that'll <em>clearly</em> win more points.</p><p>As far as ease of use and how advanced the 'prototype' would be, well, that's just all about building, so just gotta <em>execute</em> to prove those right.</p><p>Same thing as far as Completeness goes.</p><p>Now, that pesky ROI category... the project was always going to be cost-effective &#8212; as long as we look at things from an infrastructure POV &#8212; since these serverless services are basically free at the scale we use them.</p><p>But is the project worth <em>pursuing</em>? Well, it's more worth pursuing than <em>just</em> the Dank Meme Classifier, so that was good enough for me. Of course, this is almost assuredly what did me in in the end as far as not winning the grand prize, so... no.</p><p>In any case, a game about swiping memes up and down was <em>it</em>.</p><h1>The Implementation</h1><p>Once the idea had been manifested, the project just became the usual building process: Sketch out the UI. Build some Figma mockups. Put together a <em>giant</em> to-do list. And Just. Start. Grinding. </p><p>First up was getting all of the Cloud Functions for the meme ingestion pipeline re-deployed and refactored to work with Cloud Workflows. As I mentioned, they were originally built to work with PubSub, but I found Workflows basically <em>perfect</em> for this use case of passing data between a bunch of Functions. Of course, I also wanted to expand the number of Functions being used for... you know, 'judging' purposes.</p><p>Next up was building the new Frontend. Decided to go with NextJS this time just to get some more experience with it (the ongoing hotness and all that), but it was just the usual process of "build each page, make them functional, yada yada". Nothing but fun, grindy React + Sass here. Of course, making the game actually work wasn't completely trivial, but it also wasn't all too difficult once I threw <code>react-spring</code> at the problem. </p><p>And really, that's what makes an app feel "polished": lots and lots of (<code>react-spring</code>) animations. If you want the secret to wowing the judges, the answer is always "add more animations".</p><p>After several weeks of off-and-on coding, I had a working game. Much wow.</p><p>But of course, the actual app is only one component of a hackathon project. As everyone knows, it's the <em>presentation</em> that really matters. And just like for the app, I decided to take my presentation up a notch this time.</p><p>Just to compare things, <a href="https://www.youtube.com/watch?v=VV_pniw60eE" title="this">this</a> is the video I submitted for last year's hackathon.</p><p>And <em><a href="https://www.youtube.com/watch?v=DIzpaVAnMnU" title="this">this</a></em> is what I submitted this year.</p><p>This year's video is an improvement in every single way possible over last year's. </p><p>For example, last hackathon's video was literally a single take, completely made up on the fly, using basic screen recording and my laptop's built-in mic. No edits, no effects, no nothing; I just needed to record a demo as fast as possible to submit before the deadline.</p><p>This year, though, not only did I script the video... not only did I use a proper dedicated microphone... not only did I do multiple takes... not only did I screen record dedicated a-roll/b-roll... not only did I spend much more than just 10 minutes on it... </p><p><strong>I literally taught myself (the basics of) video editing and Final Cut Pro to stitch everything together.</strong></p><p>That's right, using the proceeds from my first hackathon win, I ended up buying my first Mac (an M1 Macbook Air) and unlocked access to Apple's line of Pro creative apps &#8212; Final Cut being the obviously useful one here. Quite poetic that it would end up playing a big part in 'winning' me my next hackathon.</p><p>Of course, that's just the technical side of the presentation. The creative half was coming up with the utterly 'humorous' branding and messaging. It's one thing to build a joke project; it's quite another to full-commit and market it as one.</p><p>And with that, I had a complete project submission! Twas a fun month putting everything together and learning the basics of video editing. </p><p>But now, for the final results...</p><h1>The Results</h1><p><a href="https://cloud.google.com/blog/products/serverless/serverless-hackathon-winners-announced?mc_cid=06b98bd9c0&amp;mc_eid=ea9c4edb29" title="Serverless Hackathon winners announced | Google Cloud Blog">Serverless Hackathon winners announced | Google Cloud Blog</a></p><p>Yep, I 'only' won one of the runner-up prizes (or, as they put it, "Best in Show" prizes). I can only speculate, but I would bet serious money that the reason I didn't end up taking home the grand prize was that my project idea was <em>too</em> much of a joke. Heck, the blog post even goes out of its way to say that the judges awarded the Dank League of Memeing Battlegrounds an "A+" for execution, so it only seems like the logical (and utterly unsurprising) conclusion. </p><p>In the end, was it worth it? Well, the ROI was certainly several orders of magnitude worse relative to my first hackathon win, but it was still (kinda) a win, so... probably. </p><p>Would I have done anything differently? No. <em>Should</em> I have done anything differently? Well yeah, guess I should have chosen a more serious project idea! A useful/serious project idea combined with my level execution would almost certainly guarantee a grand prize win. </p><p>But where's the fun in that?</p><p>Till next (hackathon) time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #14]]></title><description><![CDATA[It's certainly been a while.]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-14</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-14</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Sun, 02 Jan 2022 21:11:36 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!icaD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Well howdy there neighbourino! Happy New Year! </p><p>It sure has been a while since I posted here. I guess it's time to catch you up on what's been going on with the matters concerning my existence.</p><h1>Last Time</h1><p><a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-13" title="uFincs Update #13">uFincs Update #13</a> was published more than <em>4 months</em> ago. At this point, even <em>I</em> barely remember what was in it. Let's see... oh right, looks like it was about my experimentation with Capacitor for the native apps. Yeah, we'll get to the latest update on those in a bit...</p><p>But first, let me address the elephant in the room.</p><h1>Where have you been for 4 months?</h1><p>Long story short, towards the end of August, I ended up getting recruited for a full-time job.</p><p>Yes, I ended up putting uFincs on the back burner.</p><p>Why? I guess burnout, mostly. Turns out, being a solo founder is hard! Who could've guessed?!</p><p>Anyways, I basically just needed a break from working on uFincs. It just so happened that that break came in the form of working for someone else on something <em>entirely</em> different. I must say, it's been nice to have a team to talk to on a daily basis and to have a steady paycheck again &#8212; two things that were noticeably absent when working strictly on uFincs.</p><p>So why have I now returned? Well, I figure 4 months of silence is long enough as-is, so it couldn't hurt to bring in the new year with an update. That, and I've got some things to discuss.</p><p>Let's start with the good stuff: new features! Or, at least, one new feature.</p><h1>Currency Symbol Preference</h1><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!icaD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!icaD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 424w, https://substackcdn.com/image/fetch/$s_!icaD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 848w, https://substackcdn.com/image/fetch/$s_!icaD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 1272w, https://substackcdn.com/image/fetch/$s_!icaD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!icaD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png" width="1023" height="508" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:508,&quot;width&quot;:1023,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:35036,&quot;alt&quot;:&quot;A screenshot showing off the new currency symbol preference with Euro selected.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A screenshot showing off the new currency symbol preference with Euro selected." title="A screenshot showing off the new currency symbol preference with Euro selected." srcset="https://substackcdn.com/image/fetch/$s_!icaD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 424w, https://substackcdn.com/image/fetch/$s_!icaD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 848w, https://substackcdn.com/image/fetch/$s_!icaD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 1272w, https://substackcdn.com/image/fetch/$s_!icaD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F893a6624-f5fa-4594-8e64-b8b4e0bdc008_1023x508.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Yes, after oh-so-many people asked, I am finally relieving the world from being forced to use the almighty "$"! Now you can choose from &#8364;, &#163;, or even &#65509; to display across the entire app. Or, well, just about any currency in the world; it's a long list.</p><p>No, this is not "multi-currency" support; this is just allowing you to customize the (singular) currency that's displayed across the app. Multi-currency support is still not happening any time soon.</p><p>And yes, this is all I managed to accomplish for uFincs in the past 4 months. I really have been spending my time purely on New Job&#8482; and relaxing. </p><p>OK, with the obligatory new feature announcement out of the way, I want to dive into a new year's classic: the good old retrospective.</p><h1>2021 Retrospective</h1><p>How was 2021? Was 2021 a good year? Well, it certainly couldn't have been worse than 2020, so I guess, relatively, it was a good year!</p><p>But was it a good year for uFincs? Yes and no. If we go all the way back to the first uFincs posts (<a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-1" title="Update #1">Update #1</a> and <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-2" title="Update #2">Update #2</a>), we'll find my goal for the year:</p><blockquote><p>The focus of 2021 will really be getting uFincs into people's hands (aka, applying the wonder that is <strong>marketing</strong>), and getting paying customers. I'm putting it on public record that my goal is to get 100 paying customers on any paid plan (monthly, annual, or lifetime) in 2021.</p></blockquote><p>First of all, I did <em>not</em> hit that goal. At the end of the year, I've got (<em>checks notes</em>) 4 paying customers.</p><p>However, even though I didn't hit my paying customer goal, I still think 2021 can be considered a good year for uFincs. Why? Because it <em>launched</em>. Even if I had gotten zero paying customers, I'd still say this was all successful and worth it because I actually managed to put uFincs 'out there'. The fact that I acquired <em>any</em> paying customers is just the cherry on top of that (thanks everyone for your support!).</p><p>And it's not like business goals are the only thing I had set for myself. As far as technical/development goals were concerned, I did pretty damn well on them, delivering pretty much every feature I had planned (well, except for my precious command palette... and native apps... hmm). Which is really to say, uFincs is at the point where <em>I'm</em> happy with it; it can do at least everything I need it to do and then some. </p><p>So what stopped me from accomplishing more? What stopped me from hitting that glorious 100 customer goal? A few things:</p><ul><li><p>The general anxiety of launching.</p></li><li><p>My general lack of expertise in the fields of marketing/sales.</p></li><li><p>My general unwillingness to market uFincs using 'conventional' methods (e.g. paid ads).</p></li><li><p>A general feeling of burnout.</p></li></ul><p>This all just means that I clearly need to over-correct and set much less aggressive goals for 2022! Speaking of which...</p><h1>What's Next for uFincs in 2022</h1><p>Going forward, uFincs is very much going to be a "part-time" affair for me. Based on the last few months, I'd say that I'll be putting roughly 1/10th of the work in as I did in 2020/the front half of 2021.</p><p>Really, most of this comes down to uFincs not really being viable as a self-sustaining business. Could it eventually grow to be one? I can see that happening, but it's definitely one of those businesses that would grow along the <a href="https://businessofsoftware.org/talk/how-to-negotiate-the-long-slow-saas-ramp-of-death/" title="SaaS ramp of death">SaaS ramp of death</a>.</p><p>That means that my goals for uFincs (at least for now) have changed. Rather than trying to grow and acquire customers, my primary goal is now to minimize the overhead that I incur for uFincs.</p><p>What does that entail? Well, really it just means that I'm gonna be scaling back my development/marketing efforts for uFincs. That is, fewer new features and fewer blog posts.</p><p>Although this goal does put me in a small pickle about what to do with some of my backlogged blog posts. I've got some posts that were supposed to be key marketing pieces for uFincs, but now I'm not quite sure what to do with them now that I'm not trying to market uFincs. I <em>could</em> just publish them and forget about using them as marketing pieces but that just feels like a waste... Or I could just wait to publish them until after open sourcing. </p><p>Speaking of which, what do I actually intend to do with uFincs in 2022?</p><h2>Open Sourcing</h2><p>My main goal is to open source it. </p><p>Now, that might seem contradictory relative to the goal of "minimizing overhead" (and it probably is), but considering what uFincs <em>is</em> (a self-contained, privacy-focused finance app) and what I've gotten as feedback from people, open source seems to be very high up on people's wishlists.</p><p>But even beyond what <em>other</em> people want, it's what <em>I</em> want. Look, I didn't go into this business seriously thinking that I'd strike it rich (as much I might have <em>hoped</em> otherwise). As such, I've always planned for uFincs to go open-source eventually. Now it's just a matter of making that happen. There's some administrative and technical stuff that I need to take care of before that can happen, but I want it to be <em>the</em> big thing for uFincs this year.</p><p>And before anyone asks, no, the paid version of uFincs isn't going anywhere. Remember, more than anything else, <em>I</em> am uFincs' #1 customer, so <em>I'm</em> reliant on uFincs being available as a service to manage my own finances. It just so happens that others make use of the same publicly available service :) (not that I'd fault anyone for just self-hosting it instead once it goes open source)</p><h2>Native Apps</h2><p>Now, about those native mobile/desktop apps... After thinking about it a lot, I've decided not to 'officially' release them. Between all the nightmare stories I've heard about dealing with the app stores, the general extra support overhead they'd introduce, not to mention the technical overhead of publicly maintaining 4 'versions' of uFincs... frankly, I just don't want to.</p><p>However, that doesn't mean that they're not happening. I've had the code and the ability to generate the iOS/Android/Electron builds of uFincs for many months at this point (thanks <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-13" title="Capacitor!">Capacitor!</a>). So I'm going to compromise on this by keeping that code in the repo when uFincs goes open source &#8212; then anyone who <em>really</em> wants a native build can compile it for themselves.</p><p>On the one hand, from a security/privacy perspective, this is probably about as good as it gets! (building from source, that is) On the other, however, I acknowledge that it's a pretty terrible user experience. So I'm not taking the possibility of app store builds for uFincs off the table; it'll just be one of those things I deal with in the future if I ever get super ambitious again.</p><h2>Anything Else?</h2><p>No, not really. At this point, I don't want to commit to delivering any especially fancy new features or anything (which, knowing myself, means that I'm effectively committing to accomplishing <em>nothing</em>...). At this point, I very much want to just play it by ear. There's obviously a ton of things in my backlog with some features having been re-prioritized due to people's feedback, but I'm just gonna work on things slowly. </p><p>You know, "move slowly and build things".</p><h1>Final Thoughts</h1><p>2021 was an interesting year. I managed to accomplish one of my dream goals (launching a business, no matter how small). Then, in a complete twist of fate, I managed to pivot into full-time employment. Not exactly what I had in mind for finishing up 2021, but it just means that 2021 was successful in ways even I couldn't have anticipated.</p><p>Who knows what's in store for 2022. I <em>suspect</em> full-time employment will be taking up a large portion of my time but maybe open sourcing uFincs is what causes it to take off. Only time will tell.</p><p>Speaking of which... till next time :) And Happy New Year!</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #13]]></title><description><![CDATA[What comes after import rules?]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-13</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-13</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Tue, 17 Aug 2021 00:31:02 GMT</pubDate><enclosure url="https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/2441320a-3cc3-4bcd-9a72-91e0ae3029e8_150x150.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today, on matters concerning my existence, is a discussion on what the heck comes next after I spent far too much time on the Import Rules feature.</p><h1>Last Time</h1><p><a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-12" title="uFincs Update #12">uFincs Update #12</a> was just a preview of the new Import Rules feature (of which the official announcement can be found <a href="https://blog.ufincs.com/p/announcing-import-rules" title="here">here</a>). </p><p>I anticipated this being a rather large feature to implement and &#8212; lo and behold &#8212; it was! I originally estimated that it would take 4-6 weeks, then 4-8 weeks, and it ended up taking the full 8 weeks. Just another grand example of <a href="https://en.wikipedia.org/wiki/Parkinson%27s_law" title="Parkinson's Law">Parkinson's Law</a> at work!</p><p>Anyways, that's in the past. Now we have to discuss the future. What exactly is next for uFincs?</p><p>There's one large elephant in the room that I want to talk about: native apps.</p><h1>Native Apps</h1><p>A 'feature' that has been highly requested (for good reasons), native apps have been something that I've just kept pushing back more and more. This has been for primarily two reasons:</p><ol><li><p>There is a large amount of upfront work to build them, even taking into account all of the efforts I've gone to ensure that we can re-use as much code as possible.</p></li><li><p>There is a non-insignificant time cost (mostly related to development velocity) to supporting more platforms than 'just' web.</p></li></ol><p>As much as everyone (myself included) wants there to be native apps for uFincs, I've found it hard to justify their development. Sticking with 'just' a web app seems so much simpler and easier, especially for the company-of-one that is uFincs.</p><p>And while I had included them as part of my roadmap for 2021, if I was going to go about building them in React Native (my original plan), then they would have likely taken up the majority of my 2021 development time by themselves. Weighing feature development vs native app development (vs marketing vs ...) is always a tough decision, with features generally winning out with web apps since web apps technically do <em>work</em> on all platforms.</p><p>However, I recently came across a piece of technology while <a href="https://news.ycombinator.com/item?id=27807850" title="reading Hacker News">reading Hacker News</a> that, quite frankly, I probably should have already known about. However, in my defense, if <em>I</em> didn't know about it, then it certainly stands to reason that maybe <em>they</em> are lacking in their own marketing department.</p><p>In any case, I'd like to do some of their marketing work for them and introduce you to <a href="https://capacitorjs.com" title="Capacitor JS">Capacitor JS</a>.</p><h2>Capacitor</h2><p>From the team behind <a href="https://ionicframework.com" title="Ionic">Ionic</a>, Capacitor is a "cross-platform native runtime for web apps". In other words, you just plop the <code>@capacitor/core</code> package in your web app, run a command or two to bootstrap the files for an Android and/or iOS app, and now you've got your web app running in a 'native' Android/iOS app.</p><p>Quite literally.</p><p>I know there have been several attempts to achieve such an experience in the past, but wow, Capacitor is the first one I've tried that actually <em>nails</em> it. Heck, it took me longer to get a stupid Android emulator working than it did it to integrate Capacitor into uFincs!</p><p>Within maybe half an hour, I'd gone from "installing Capacitor" to "uFincs running as a native app on an emulated device". Now, to be fair, that half an hour did not include having uFincs <em>working</em> on a native device (I only said <em>running</em>). There was still a bit more work to be done to get all of the networking config in place so that the app could talk to my local API server (which, again, is more the fault of the Android development experience than anything to do with Capacitor). </p><p>But still. That turnaround time is <em>insane</em> compared to what it would take to, say, rebuild the whole app (mainly the UI) in React Native. I'm talking like 1 hour vs several <em>months</em> here.</p><p>So how does Capacitor do it? Well, as far as I can tell, it's not especially more innovative than other solutions have been in the past. It basically just takes the built static assets from your web app and serves them inside a web view that is rendered within the native app. It also does a bunch of bridging to allow the web code to talk to native code for features like push notifications (aka stuff I don't really care about for uFincs). The core idea is still just "run the web app in a web view with a native shell", but with a more refined and polished development experience.</p><p>Obviously, if uFincs didn't already have a mobile-optimized design, then all this Capacitor magic would be a rather moot point. So you could argue that the many months I spent in 2020 redesigning the uFincs UI would be where all the 'work' has gone.</p><p>But all I really care about right now is the <em>marginal</em> work it would take to go from "uFincs as a web app" to "uFincs as  a native app that can be installed from the app stores". In this case, Capacitor seems to shrink that time down <em>dramatically</em>, again relative to my original plan of using React Native.</p><h3>The Spike</h3><p>In that vein, I decided to take on the Capacitor integration as a 'spike' (aka a short-term experiment or proof-of-concept, but you know how those Agile folks love their buzzwords). After my initial experimentation with getting Capacitor working with Android, I wanted to see if I could then get it working on iOS and desktop, and then get it as close as possible to a 'publishable' version (aka iron out the kinks).</p><p>iOS ended up taking a bit more work, mostly due to styling issues. Since the web view on iOS is still just Safari, I ended up having to deal with all the same quirks/bugs I ran into when first testing on Safari. They all happened again because the "is Safari?" logic that I had relied on checking the user agent (an already unreliable indicator), but the iOS web view didn't have the same user agent. Thankfully, it was mostly just a matter of adding a <code>Capacitor.getPlatform() === "ios"</code> check.</p><p>Additionally, I decided to switch from using IndexedDB to using Capacitor's <a href="https://capacitorjs.com/docs/apis/storage" title="Storage plugin">Storage plugin</a> for <code>redux-persist</code>'s storage backend, just as a more reliable way for storing user data. I know that Capacitor doesn't recommend using the Storage plugin for large amounts of data, but I'll need to do some further performance testing to see if it really hampers anything as a user scales to thousands of transactions.</p><p>Another big change I did (for Android and iOS) was remove every mention of signing up or subscribing. Not from past experience or anything, but I know that the app stores have been particularly stringent when it comes to managing paid subscriptions through anything but their own payment services. I figured preemptively removing all such mentions would save me some headaches in the future (if you want such an example of a headache, check out <a href="https://news.ycombinator.com/item?id=28172490" title="this latest situation">this latest situation</a>). </p><p>Yes, this makes for a worse user experience (since one could download the app and be greeted with only an option to log in but not sign up), but since we provide the ability to use the app without an account, I think the compromise is worth it.</p><p>As far as desktop goes, while Capacitor doesn't have first-class support for something like Electron, there <em>is</em> a <a href="https://github.com/capacitor-community/electron" title="community built plugin">community-built plugin</a> for adding Electron as a Capacitor platform. Which, I will say, worked basically flawlessly &#8212; just as the Android and iOS platforms did.</p><p>The final 'hurdle' was changing out the app icons for our own. There's an officially supported tool called <a href="https://github.com/ionic-team/cordova-res" title="cordova-res">cordova-res</a> that can handle generating the necessary icon assets from a base file, but I found it to not generate the best assets for Android (they were sized incorrectly); I ended up just using Android Studio's built-in <a href="https://developer.android.com/studio/write/image-asset-studio" title="asset generator">asset generator</a> for the Android app. The iOS icons turned out fine, however.</p><p>Altogether, after a week's worth of work and play, the "Capacitor spike" resulted in the ability to build uFincs as a native Android app, a native iOS app, and an Electron app for Windows, Mac, <em>and</em> Linux. AKA, everything that matters. </p><p>However, this was only the <em>spike</em> &#8212; the beginning. I don't think the native apps are yet ready to be published, but I'll elaborate on that in a bit.</p><p>First, I want to take a short minute to discuss <em>the catches</em>.</p><h3>The Catches</h3><p>So what are <em>the catches</em>? Surely there <em>must</em> be catches right? </p><p>Well, it depends on how pedantic and nitpicky you want to be.</p><p>As much as the app that Capacitor generates is indeed a 'native' app (from a binary/distribution point-of-view), it doesn't make use of the traditional native UI elements. So if you're an utter stickler for only using those widgets which have been blessed by the designers-that-be at Google and Apple, then you probably think this approach is pretty sacrilegious!</p><p>However, that's a <em>mostly</em> moot point as far as uFincs is concerned, because if I <em>were</em> to build it in React Native, I certainly wouldn't be going out of my way to re-re-design the UI to match each system's native elements. <em>That</em>, my friends, is too much.</p><p>Other than that, I suppose there might be some superficial performance penalties to choosing this hybrid approach. Not that there wouldn't also be any with React Native.</p><p>So... I don't know what the catches are yet. Personally, I think this Capacitor approach is just pure upside. I get to re-use the exact same code base, everyone gets the exact same uFincs experience, but I still get to distribute on the native app stores. </p><p>Plus, from a security standpoint, this approach still gives me the native app 'benefit' of not worrying about the Frontend server being compromised and immediately serving a version of the app that breaks all of the encryption features. Of course, if I <em>really</em> wanted to trash my company and entire career, I (or a malicious actor) could still push a compromised update through the app stores, but for those that want an "install once" experience, the Capacitor approach still affords that.</p><p>So, with only a week's worth of Capacitor experience under my belt (and not even a published app to my name), I can super confidently say that <em>there are no catches</em>.</p><h2>The Next Steps</h2><p>While uFincs has now been made 'technically' fully functional as a native app, as is true of all things, the 80/20 rule still applies. They're still a few wrinkles to iron out. </p><p>Small things like "how do you fetch the latest data?". With the web app, you can just refresh the page and all your latest data is pulled down. Well, you can't exactly "refresh the page" with a native app, so we'll need either a button to do it manually or a process that runs in the background (probably both).</p><p>Then there are things like setting up a new CI/CD pipeline. I mean, since the code is all the same, that also means that all the same tests should apply, but how exactly do we handle things like "continuous deployment" with mobile apps? I don't think we actually do, but it's the <em>process</em> of figuring out how we're going to handle deployment/distribution that is important.</p><p>Speaking of which, app stores. God, app stores. I have been dreading the moment I'd have to start dealing with them. It's one thing having a web app where I have complete and utter control over the entire deployment process, being able to deploy anything and everything at the push of a commit. It's quite another having to submit every update to entities that are known for being both much slower and much less reliable than my precious build pipeline.</p><p>In the best case, <em>maybe</em> the uFincs native apps will be available (as a beta) within the next month or two. Assuming I don't get arbitrarily rejected from the app stores for no reason. &#175;\_(&#12484;)_/&#175;</p><h1>Final Thoughts</h1><p>Other than the native apps, I've been slowly getting through some other (much smaller) items from my backlog. Check out the <a href="https://ufincs.com/changelog" title="uFincs changelog">uFincs changelog</a> for all the details.</p><p>As I've mentioned in the past, I've also been planning another big marketing push. If I can get the native apps into beta, then that'll probably happen sometime around September/October. Also got some blog posts that I need to finish up for that. Hopefully, this push will be enough to pick up a couple more customers. </p><p>Can never have enough customers :) </p><p>Till next time.</p>]]></content:encoded></item><item><title><![CDATA[Announcing Import Rules]]></title><description><![CDATA[...but not here]]></description><link>https://www.onmattersconcerningmyexistence.com/p/announcing-import-rules</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/announcing-import-rules</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Fri, 30 Jul 2021 19:54:38 GMT</pubDate><enclosure url="https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/2441320a-3cc3-4bcd-9a72-91e0ae3029e8_150x150.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey there dear reader of ye olde blog of mine!</p><p>Just wanted to let you know that, if you aren't (yet) a subscriber to the&nbsp;<a href="https://blog.ufincs.com/">official uFincs blog</a>, the post announcing the&nbsp;<a href="https://blog.ufincs.com/announcing-import-rules" title="official release of Import Rules">official release of Import Rules</a>&nbsp;has been put up over there.&nbsp;</p><p>That is all. Please continue on with your day.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #12]]></title><description><![CDATA[Previewing the Import Rules System]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-12</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-12</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Mon, 19 Jul 2021 23:36:20 GMT</pubDate><enclosure url="https://cdn.substack.com/image/fetch/h_600,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The matter concerning my existence today is a quick update on the import rules system for uFincs.</p><h1>Last Time</h1><p>During <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-10" title="uFincs Update #10">uFincs Update #10</a>, I gave a short update on the rules system just as development was really ramping up. Now that it's more or less fully functional, I'll be previewing it in preparation for launch (hopefully) sometime at the end of the month.</p><h1>Import Rules System</h1><p>If you want to jump straight into trying it out, you can use a 'staging' version of uFincs here: <a href="https://ufc-381.ufincs.com" title="https://ufc-381.ufincs.com">https://ufc-381.ufincs.com</a>. If you have an account with uFincs, then you can log in using those same credentials. Don't worry; any changes you make here won't be reflected against your actual account's data.</p><p>The first thing you'll notice is that the "Import Transactions" option in the "Add" button menu no longer takes you directly to the import process. Now there's this sort of 'overview' page:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Pk7z!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Pk7z!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 424w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 848w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Pk7z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/bc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:248160,&quot;alt&quot;:&quot;The newly designed \&quot;Import Overview\&quot; page. It is the hub for choosing an import option and managing your import rules.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The newly designed &quot;Import Overview&quot; page. It is the hub for choosing an import option and managing your import rules." title="The newly designed &quot;Import Overview&quot; page. It is the hub for choosing an import option and managing your import rules." srcset="https://substackcdn.com/image/fetch/$s_!Pk7z!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 424w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 848w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!Pk7z!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc36861c-82c0-4cf1-9ba8-8e42e10182f9_3360x1892.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This overview page combines the new view for the import rules with the options for importing transactions. Currently (as was previously the case) we only support importing from CSV files, but this new design gives us a good place to put any future import options.</p><p>Let's take a closer look at the rules. Here's the new form for creating/updating a rule:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LnMO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LnMO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 424w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 848w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LnMO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png" width="262" height="619.63" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/bc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1892,&quot;width&quot;:800,&quot;resizeWidth&quot;:262,&quot;bytes&quot;:108568,&quot;alt&quot;:&quot;The new Import Rule form.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The new Import Rule form." title="The new Import Rule form." srcset="https://substackcdn.com/image/fetch/$s_!LnMO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 424w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 848w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!LnMO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fbc39cb93-6805-47a8-ab66-945ff539d932_800x1892.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>An import rule is quite simple: it consists of a set of conditions and a set of actions. If, during import, a rule's conditions match for a certain transaction, then the rule's actions will be applied to the transaction.</p><p>I've kept things quite simple for this initial implementation. A condition can match against a transaction's Account or Description property, using either a plain string or a regex. An action can set one or more of a transaction's Account, Description, or Type. </p><p>That's it. </p><p>However, even with this limited set of actions and conditions, I expect that this will still cover a large majority of use cases. After all, the import process is usually just a matter of cleaning up some transaction descriptions and categorizing them to the right account. These rules should help to drastically cut down on any repetitive importing or to help with importing a large number of transactions all at once.</p><p>Of course, if there are any other conditions or actions that you want supported, just let me know!</p><p>Once a rule has been created, here's what it looks like in the table:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MQ0u!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MQ0u!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 424w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 848w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 1272w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MQ0u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png" width="1456" height="510" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:510,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94135,&quot;alt&quot;:&quot;An example rule displayed in table format.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="An example rule displayed in table format." title="An example rule displayed in table format." srcset="https://substackcdn.com/image/fetch/$s_!MQ0u!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 424w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 848w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 1272w, https://substackcdn.com/image/fetch/$s_!MQ0u!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F5ee1169c-4866-4ec2-a2de-b80c46a00919_2136x748.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Yeah, I'll admit, it's not the prettiest thing. But it gets the job done.</p><p>Finally, here's what the rules look like during the import process itself. First, you'll find a new section during the fourth step (Adjust Transactions):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!J7f4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!J7f4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 424w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 848w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 1272w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!J7f4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png" width="1456" height="818" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:818,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:1289854,&quot;alt&quot;:&quot;The Adjust Transactions step with a red arrow pointing at the new \&quot;Active Import Rules\&quot; section.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The Adjust Transactions step with a red arrow pointing at the new &quot;Active Import Rules&quot; section." title="The Adjust Transactions step with a red arrow pointing at the new &quot;Active Import Rules&quot; section." srcset="https://substackcdn.com/image/fetch/$s_!J7f4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 424w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 848w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 1272w, https://substackcdn.com/image/fetch/$s_!J7f4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F2e47c70a-1b4c-4e84-9567-d6c1a47ff20d_3360x1888.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And you can open it up to reveal a list of the currently active import rules:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tQP0!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tQP0!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 424w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 848w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tQP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png" width="1456" height="820" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/0473256f-6788-4628-9a73-d699563aa284_3360x1892.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:820,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:408068,&quot;alt&quot;:&quot;The Adjust Transactions step with the \&quot;Active Import Rules\&quot; section open.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The Adjust Transactions step with the &quot;Active Import Rules&quot; section open." title="The Adjust Transactions step with the &quot;Active Import Rules&quot; section open." srcset="https://substackcdn.com/image/fetch/$s_!tQP0!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 424w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 848w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 1272w, https://substackcdn.com/image/fetch/$s_!tQP0!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F0473256f-6788-4628-9a73-d699563aa284_3360x1892.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>These are all of the rules whose conditions matched up with any of the transactions that you're currently importing. </p><p>If you want to add any other rules, you can also do that here. </p><p>Finally, you can toggle the rules on or off, in case you need to reference the original values  from your CSV file.</p><p>That's all I've got in terms of the preview. While the functional implementation is basically done at this point, I'm still in the weeds when it comes to testing. There's a lot of weird ways the rules system can be abused to create invalid transactions, so I'm definitely trying to cover as many of those corner cases as possible before releasing this out to everyone.</p><p>In the meanwhile, feel free to try it out and let me know what you think!</p><p>Till next time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #11]]></title><description><![CDATA[DevOps Detour - Part 4 (The Finale)]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-11</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-11</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Sun, 04 Jul 2021 18:46:07 GMT</pubDate><content:encoded><![CDATA[<p>OK everyone, it's time for the fourth and final installment of the DevOps Detour series. We're already several months out from when these events actually took place, but nonetheless, I want to close out this series.</p><p>Today, the matter concerning my existence is how one goes about making their Docker images more secure &#8212; and the fallout from trying to do that.</p><h1>Last Time</h1><p>The <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-8" title="third part">third part</a> of the DevOps Detour was all concerned with upgrading <code>cert-manager</code> and <code>ingress-nginx</code>, and how I thought the <code>cert-manager</code> upgrade would take forever when it ended up being the easy one of the two.</p><p>We're staying on the theme of "upgrading" today, but just shifting it slightly from upgrading services to upgrading how we handle images themselves. The topics of today are distroless images, multi-stage builds (and how caching them sucks), and Docker BuildKit (and how it makes caching suck).</p><h1>Distroless Images</h1><p>For the longest time, all of the services for uFincs used <code>node:14-alpine</code> images. Alpine images have long been my default image type of choice (as they have for a lot of people) mainly due to their smaller sizes.</p><p>However, that all changed when the Distroless attacked.</p><p>I don't remember where I first learned about <a href="https://github.com/GoogleContainerTools/distroless" title="distroless images">distroless images</a> (probably just one of the GCP security docs), but these things are damn cool. Whereas Alpine images run... well, Alpine Linux, and are generally just smaller, Distroless images run <a href="https://github.com/GoogleContainerTools/distroless#base-operating-system" title="Debian">Debian</a> but with basically nothing included. Like, there's no shell tools like <code>cd</code> or <code>ls</code>. Heck, there's not even a shell, <em>period</em>. </p><p>The only thing the image includes are the runtime dependencies necessary for the app &#8212; in my case, using the Node images, that means <code>node</code> itself. Notably, it <em>doesn't</em> include <code>npm</code> ("lol wut", you might be thinking, but we'll get back to this in a bit).</p><p>And why might one want to use these severely limited images? As you might have guessed, it's all in the name of security.</p><p>By drastically reducing the attack surface area, attackers have much less room to work with. Think about it: if an attacker were to directly compromise one of our services, what on Earth are they going to do if they <em>don't even have a shell??</em> They'd have better luck trying to social engineer their way into my GCP account. And good luck with that!</p><p>Having our services this locked down is important because our greatest security risk is if the service serving the uFincs app files (aka our Frontend service) becomes hacked and starts serving files that compromise the cryptographic integrity of a user's data. Combined with using non-root users and a read-only file system (among other things), it would take a <em>major</em> security vulnerability to directly compromise our Frontend (or any of our services).</p><p>"This is all well and good (you can never have enough security, right?), but what about <code>npm</code>? How do you install anything without it?"</p><p>A very good question. The answer? Multi-stage builds.</p><h1>Multi-Stage Docker Builds</h1><p>At this point in Docker's life, multi-stage builds are nothing new or innovative. However, <em>I</em> hadn't yet adopted them, so they were pretty "new" to me (not because I didn't know what they were &#8212; just that I hadn't found a use for them yet).</p><p>And why hadn't I adopted them yet? Well, in most of the documentation/articles I'd read around multi-stage builds, it was usually in the context of compiled languages. It was examples like Go where you could create a compiled binary file in the first Docker stage and then just copy it over and execute it in the second stage.</p><p>However, since I'm primarily a Node developer &#8212; and JavaScript isn't exactly a compiled language &#8212; I never thought that it was really worth it since you'd still need the <code>node</code> executable to actually run anything. And since you'd need <code>node</code>, you'd need all the tooling that is required to first <em>install </em><code>node</code> in the first place (e.g. a <em>shell</em>), which just seemed like too much work for too little benefit.</p><p>It only 'clicked' once I read through some of the Distroless examples. By using a multi-stage build, you could use a regular Node image in the first stage (in my case, still <code>node:14-alpine</code>) to install <code>node_modules</code> and do any compilation/linting/testing steps (in my case, 'compilation' because of TypeScript), and then copy over the code and <code>node_modules</code> into a Distroless image in the second stage, where the app server would then be directly executed by <code>node</code>. Since we're copying over the installed <code>node_modules</code> as well, that solves the problem of not having <code>npm</code> in the Distroless image!</p><p>Well, mostly. The only <em>slightly</em> annoying thing with not having <code>npm</code> in the final Distroless image is that this also meant not being able to run our <code>npm</code> scripts. Whereas the app server is just an <code>index.js</code> file that can be directly executed by <code>node</code>, our <code>npm</code> scripts were... slightly more complicated, since they called out to installed dependencies in <code>node_modules</code>. </p><p>This was really only noteworthy because our database migration script was an <code>npm</code> script, and it had to be run using the production (i.e. Distroless) image because it was run during our CI/CD pipeline.</p><p>So what'd I have to do? Well, I couldn't write a shell script to replace it since... there was no shell. So I ended up having to write a <code>node</code> script that would call out to the other <code>node_modules</code> executables. It was... not pretty. But it worked!</p><p>Another side effect of using multi-stage builds (and distroless images and not having <code>npm</code>...) was that we now had different images for running in development and running in production. We kept a <code>node:14-alpine</code> image for development since we still needed all of our dev tooling available for.. development. This is obviously less than ideal as far as a "prod/dev" mirroring strategy goes, but since we have the wonder that is per-branch deployments (courtesy of <a href="https://github.com/DevinSit/kubails" title="Kubails">Kubails</a>), we at least still have a 'staging' environment that damn near mirrors the production environment for testing.</p><p>And with that, everything was great! We now had much more secure production images so that no one would ever hack us. There was absolutely nothing wrong with this scheme. Nope, none at all...</p><h1>Caching Multi-Stage Builds</h1><p>Sike.</p><p>In hindsight, this is plainly obvious, but in the moment, it was a real pain in the ass.</p><p>See, with multi-stage Docker builds, each stage is itself an image. But if you want to use regular image caching (i.e. specify an already-built image to use as the basis for building a new image), then you have to store images from <em>every</em> stage. Unfortunately, you can't just store the final (distroless) image and expect that it somehow has the layers from the first stage and will speed up its build. And since the first stage is the one that takes 99% of the build time, it's doubly unfortunate.</p><p>As such, I had to modify our build pipeline to push and pull the first stage images to make sure they get the cache speedup. Otherwise, each build would end up being 30+ minutes.</p><p>However, this <em>still</em> wasn't the end of it. As part of this overall "upgrading" process, I also ended up upgrading parts of Kubails &#8212; one of which was Docker itself. And it seemed like Docker decided to introduce a new "build engine" that would throw me for a loop. </p><p>Let me introduce to you: Docker BuildKit.</p><h2>Docker BuildKit</h2><p>As <a href="https://www.docker.com/blog/advanced-dockerfiles-faster-builds-and-smaller-images-using-buildkit-and-multistage-builds/" title="Docker">Docker</a> puts it, BuildKit is a new "builder backend". Among other things, they say they introduced it because it improves image build times through the use of parallelism.</p><p>Quite frankly, there's a whole bunch of other changes that BuildKit introduces to the Docker ecosystem, but I want to cut straight through all of that. What's most important for me, here and now, is the fact that enabling BuildKit is done by adding the <code>DOCKER_BUILDKIT=1</code> environment variable to any Docker commands (e.g. <code>DOCKER_BUILDKIT=1 docker build ...</code>) and that images can't be used for caching <em>by default</em> when doing that.</p><p>I don't know who decided it or why (it was frustrating enough to figure out how this worked the first time that I didn't want to look into it any further), but if you want an image to be useable as a cache image in a future build (i.e. as a <code>--cache-from</code> argument), you need to add <code>--build-arg=BUILDKIT_INLINE_CACHE=1</code> when building the image. That is, a minimal build command now looks like this:</p><pre><code>DOCKER_BUILDKIT=1 docker build --build-arg=BUILDKIT_INLINE_CACHE=1 .</code></pre><p>As I understand it, you need to explicitly include the 'cache information' as part of the image, otherwise using an image built with BuildKit (but <em>without</em> the build arg) will do literally nothing when specified under <code>--cache-from</code>. </p><p>Something else worth noting is that, normally, you can just specify the <code>--cache-from</code> image and Docker will handle pulling the image for you. However, and I don't know if this was just a bug at the time or something, but it would <em>not</em> work for me. Running in GCP Cloud Build with GCP Container Registry (notably, <em>not</em> Artifact Registry), I had to explicitly pull each <code>--cache-from</code> image before it could be used as a cache image; the build would just error out otherwise (I don't remember what the error was, sorry). So that was also a royal pain to debug.</p><p>Hopefully, I just saved you many hours of Googling and rebuilding images.</p><p>Questions you might be having:</p><ul><li><p>"Why don't you just not enable BuildKit? Won't images work as cache images like before you upgraded Docker?"</p><ul><li><p>I thought so, and you would <em>think</em> so, but they didn't <em>seem</em> to. I don't know if it was just because I happened to upgrade Docker at the same time that I started using multi-stage builds, and that I was just doing something wrong, but I couldn't get the old image caching behaviour to work.</p></li></ul></li></ul><p>Anywho, once I had BuildKit somewhat figured out, the multi-stage builds were finally being cached properly, which means we could finally enjoy all the wonderful security benefits of Distroless images.</p><h1>Final Thoughts</h1><p>Honestly, if I had known how much tinkering it would take to get the whole multi-stage build process working, I probably would have just settled for just non-root users and read-only filesystems. I feel like that would have gotten us like 80% of the security benefits without having to completely retool a large portion of our build pipeline. But you know what they say: defense in depth!</p><p>And with that, the DevOps Detour series has finally concluded. We've improved our backup system, improved the security of our Kubernetes cluster and services, and got all fancy with our Docker images. These improvements give just that little bit of peace of mind that <em>maybe</em> things won't horribly blow up in the middle of the night. <em>Maybe</em>.</p><p>Till the next (non DevOps) time.</p>]]></content:encoded></item><item><title><![CDATA[How I Won a Month-Long Hackathon in 3 Hours]]></title><description><![CDATA[By cheating*]]></description><link>https://www.onmattersconcerningmyexistence.com/p/how-i-won-a-month-long-hackathon</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/how-i-won-a-month-long-hackathon</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Wed, 23 Jun 2021 18:31:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/youtube/w_728,c_limit/VV_pniw60eE" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>*Terms and conditions apply (quite literally)</em></p><p>I'm not much of a hackathon guy. Partly because I see them as a waste of time, partly because I'm far too pessimistic to think that I'd be able to put together anything 'win-worthy' in such a short period of time.</p><p>But on <strong>March 16th</strong>, I came across&nbsp;<a href="https://www.reddit.com/r/googlecloud/comments/lmzmqo/official_google_cloud_code_love_hackathon_10k/" title="this post">this post</a>&nbsp;on the GCP subreddit:</p><blockquote><p>Official Google Cloud Code_Love_Hackathon | 10k Prizepool</p></blockquote><p>Being someone who is fairly experienced in the ways of GCP (formerly Associate Cloud Engineer/Professional Cloud Architect certified), I figured this would be a fun break from the never-ending marathon that is building a startup (<a href="https://ufincs.com/" title="uFincs">uFincs</a>, by the way).</p><p>The&nbsp;<a href="https://codelovehack.bemyapp.com/" title="hackathon overview">hackathon overview</a>&nbsp;outlined the details:</p><ul><li><p>4 different project categories:</p><ol><li><p>"The Next Level of Love" = build an app that makes developer lives easier</p></li><li><p>"The Next Level of Gaming" = build a game of some sort</p></li><li><p>"Spreading the Love" = build a messaging app of some sort</p></li><li><p>"Wildcard" = build whatever you want, duh</p></li></ol></li><li><p>Besides the per-category themes, you also had to make use of the new GKE "Autopilot" mode, along with the GCP Cloud Code IDE extension</p></li><li><p>A prize pool of $10k USD distributed as $2k to each category winner, along with $2k in a bonus category for "Best use of GKE Autopilot"</p></li><li><p>Lasts from <strong>March 29th</strong> to <strong>April 21st</strong> (basically a month, if you include pre-official start time)</p></li></ul><p>First of all, this is the first month-long hackathon that I've ever seen. I mean, I guess it makes sense given the (virtual) times we live in, but still, I thought that was a pretty excessive timeline.&nbsp;</p><p>Secondly, the requirements seemed rather easy. I'm decently experienced with GKE, so just making it&nbsp;<em>easier</em>&nbsp;by tacking on Autopilot meant that all I had to worry about was choosing a category to enter in.</p><blockquote><p>For reference, GKE's new&nbsp;<a href="https://cloud.google.com/kubernetes-engine/docs/concepts/autopilot-overview" title="&quot;Autopilot&quot; mode">"Autopilot" mode</a>&nbsp;just relieves you, the cluster operator, from having to worry about managing nodes. Instead of getting billed based on nodes, you get billed based on how much CPU/RAM each pod is allocated (except it costs a&nbsp;<em>lot</em>&nbsp;more, strictly comparing compute resources).</p><p>Just another layer of abstraction in the never-ending game of "The Cloud".</p></blockquote><p>Finally, while $2k per category is a pretty nice chunk of change, it definitely wasn't enough to warrant building a fully custom project and&nbsp;<em>certainly</em>&nbsp;didn't warrant a month's worth of work.</p><p>As the start date of the hackathon approached, I brainstormed some ideas but ultimately decided that it wouldn't really be worth my time. The better ROI here was prepping uFincs for launch, so I decided to just continue on with my marathon instead.&nbsp;</p><p>In the meantime, I registered for the hackathon and joined the related Slack team just to keep an eye on updates (and on the competition).</p><h1>One Month Later</h1><p>A month has passed. I had just finished up some big feature development work for uFincs along with dealing with some ops-related tasks that I had been putting off (check out the start of the&nbsp;<a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-5" title="DevOps Detour series">DevOps Detour series</a>&nbsp;if you're interested in that). I was well on track for launching uFincs in the next week.</p><p>It was purely coincidental that on <strong>April 21st</strong>, the final day of the hackathon, I checked into the Slack channel to see an update&nbsp;<em>saying</em>&nbsp;that it was the last day of the hackathon. It was around 7:00 PM at this time, and the hackathon finished at 3:00 AM my time.&nbsp;</p><p>I made the fortunate decision to log in to the hackathon platform and check out what projects had been submitted. With only ~8 hours to go, surely most teams would have submitted by now (or not, depending on what kind of person you are). And you know what I saw?</p><p>A fully open category.</p><p>While 3 of the 4 categories had a smattering of submitted projects (no more than 3 or 4 each), the "Next Level of Gaming" category had zero entrants.</p><p>This was my opportunity.</p><p>If I could submit something,&nbsp;<em>anything</em>, to this category, then I'd be the winner! Obviously. It was just a small matter of submitting&nbsp;<em>what</em>...</p><p>Well, this is where the 'cheating' comes into play. See, the&nbsp;<a href="https://drive.google.com/file/d/1Ey0Ezn10tXEBMApJE8OjBs08lMA6uhR_/view" title="official rules">official rules</a>&nbsp;for the hackathon didn't say that you couldn't re-use code from an existing project &#8212; all you needed was the IP rights to submit the project (heck, you didn't even have to submit any source code).&nbsp;</p><p>Now, is re-using old (open-source) projects against the spirit of a hackathon? Probably.</p><p>But money.</p><p>So, digging into my portfolio of&nbsp;<a href="https://github.com/DevinSit" title="GitHub projects">GitHub projects</a>, I narrowed it down to two:</p><ol><li><p><a href="https://github.com/DevinSit/dank-meme-classifier" title="The Dank Meme Classifier">The Dank Meme Classifier</a></p></li><li><p><a href="https://github.com/DevinSit/the-buzzword-bingo" title="The Buzzword Bingo">The Buzzword Bingo</a></p></li></ol><p>Re-purposing the Dank Meme Classifier into a game of sorts was one of the ideas I had had when first brainstorming for the hackathon, but considering the time constraints, I didn't think it'd be wise to attempt anything of the sort. So I decided that The Buzzword Bingo would be perfect, considering it was already a game!</p><p>In fact, The Buzzword Bingo was also already containerized. So really, all I had to do was spin up a new GKE cluster, deploy it, and call it a day!</p><p>And at 7:00 PM, that's what I decided to do: I committed myself to getting this project back up and running and submitting all of the necessary components of the hackathon project (namely: some marketing fluff, a demo video, and some slides).</p><p>First things first was provisioning a new Autopilot cluster to deploy on. I decided to spin up a separate GCP project to keep everything contained (and so that I could easily tear it all down later). I used&nbsp;<a href="https://github.com/DevinSit/kubails" title="Kubails">Kubails</a>&nbsp;(my home-grown framework for developing GKE-native apps) to provision a new project and quickly deployed a new cluster using the generated Terraform configs &#8212; although I had to do some minor tweaks to get the Autopilot settings working since it conflicted with some existing node pool configs.</p><p>Within a couple minutes, my new cluster was up and running. I tried to deploy my usual stack of&nbsp;<code>cert-manager</code>&nbsp;and&nbsp;<code>ingress-nginx</code>, but ran into some more conflicts with Autopilot mode. If I remember correctly, one of the deployments tried to make some changes to&nbsp;<code>kube-system</code>, but that namespace seemed to be locked down in Autopilot clusters. I can understand why, but it did make my life a bit harder.</p><p>Well actually, it ended up making my life&nbsp;<em>easier</em>. By foregoing&nbsp;<code>cert-manager</code>&nbsp;and&nbsp;<code>ingress-nginx</code>, I was able to just deploy the two services that make up The Buzzword Bingo (a backend WebSocket server and a frontend React app) directly as deployments with individual load balancer services. It was gonna cost me more to run it (since using&nbsp;<code>ingress-nginx</code>&nbsp;would have meant only needing a single load balancer), but this was only going to be for a short time anyways. And with a $2k prize on the line, I figured I could afford it.</p><p>And you know what else I figured I could afford? A new domain name. See, I actually do have an always-available version of&nbsp;<a href="https://thebuzzwordbingo.com">The Buzzword Bingo</a>&nbsp;(for portfolio purposes), but I run it on GCP Cloud Run because it's&nbsp;<em>basically</em>&nbsp;free (haven't been charged for it yet and I hope you, dear readers, don't end up costing me anything :). But since the version running for the hackathon would be on a different IP, I figured a new domain name couldn't hurt.&nbsp;</p><p>I finally decided on a minor re-branding:&nbsp;<a href="http://lovelybuzzwordbingo.com/" title="">http://lovelybuzzwordbingo.com</a>&nbsp;(now defunct). Why "Lovely"? Because the whole theme of the hackathon was "love", and if I was&nbsp;<em>really</em>&nbsp;gonna phone it in, I might as well make this the one on-theme change that I would make. Cause I certainly didn't have the time nor care to make any other changes.</p><p>With the domain name bought, the load balancers balancing, and the DNS records&#8230; resolving, by 8:00 PM, the Lovely Buzzword Bingo was now open for business!</p><p>That was the 'hard' part. Or 'less easy' part. I mean, I can deploy GKE apps in my sleep at this point (or at least, my Cloud Build pipelines can), so the only thing left to do was write up all the marketing fluff for "what the project is", "what problem it solves", "how it does it", yada yada. Having spent the last few months writing marketing copy for uFincs, that wasn't much of a problem (although my many years of bullshitting-through-school-projects certainly helped as well).</p><p>Of course, it was oh-so-much easier to write these kinds of things when you're not taking it seriously. I mean, the app in and of itself is pure "not taking it seriously", so at least we were on theme for that (for reference, I originally made The Buzzword Bingo as a joke&#8230; for a presentation&#8230; in an ethics course&#8230; in university).</p><p>The only minorly annoying part of the submission requirements was a 3-5 minute 'pitch' video showing off the project. Since we didn't even have to submit the source code, I assumed that this was basically all the judges were basing their decisions on. So I'd have to be utterly deliberate about thoroughly explaining every minute detail of the project.</p><p>Or something like that. I'll let you be the judge of just how terrible my pitch was:</p><div id="youtube2-VV_pniw60eE" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;VV_pniw60eE&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/VV_pniw60eE?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><blockquote><p>No script, no edits, one take &#8212; just the way I like it.</p></blockquote><p>In fact, I'll let you be the judge of my whole submission: everything I submitted can be found at the end of this post.</p><p>Ultimately, by 10:00 PM, I had my video recorded, my marketing fluffed, and my slides hastily thrown together. Clicked submit and called it a night, hoping that no one else would submit any more 'gaming' projects and that I'd be the de facto winner.</p><h1>The Next Day</h1><p>I woke up the next morning to have my worst fears realized: there were more gaming submissions. Not one. Not two. But&nbsp;<em>three</em>&nbsp;more submissions had shown up in the wee hours of the night.&nbsp;</p><p>Ah crap. There go my chances.</p><p>Looking over the other submissions (which I won't include here because of 'legal' reasons), some of them definitely looked like they had&nbsp;<em>work</em>&nbsp;put into them. Like, one of them had a whole story for how they came up with the idea, beta tested it, implemented it, and further conducted user testing. I honestly thought that that project had it in the bag.</p><p>The worst part of all this is that the gaming category didn't even end up having the fewest submissions; one of the other categories only had two!</p><p>Sigh. At least I didn't waste much time putting the whole thing together. And it was certainly hilarious to imagine what the judges would think when they would inevitably have to gaze upon my monstrosity. But I definitely didn't have any hopes of winning at this point.</p><p>So I just kind of forgot about the whole hackathon after that. Judging would happen over the next week with the winners being announced on <strong>May 3rd</strong> as part of a Kubernetes conference. Maybe I'd remember to check the Slack sometime in the future to see how it went down.</p><p>Back to uFincs, I suppose.</p><p>(And back to uFincs I went, where I finally launched and gained my first paying customers. Check out that story here:&nbsp;<a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-7" title="uFincs Update #7">uFincs Update #7</a>)</p><h1>May 3rd</h1><p>Remember, I had completely forgotten about the hackathon at this point. I was just riding off the high of more-or-less successfully launching uFincs to the world.</p><p>So when I received the following email from one of the hackathon coordinators, you could say that I was surprised:</p><blockquote><p>Hey Devin,&nbsp;<br></p><p>Thank you all for participating in the {Code_Love_Hack}!&nbsp;<br><br>On behalf of Google, I wanted to let you know that we appreciate all the time, effort and dedication that went into creating each amazing project. The judges had so much fun seeing all the apps and games you created!<br><br><strong>Without further ado, here are the {Code_Love_Hack} winners:<br></strong><br><strong>Challenge #1 - The Next Level of Love<br></strong>Team Member: [redacted]<br>Project: [redacted]<br><br><strong>Challenge #2 - For the Love of Gaming<br></strong>Team Member: Devin Sit<br>Project: Lovely Buzzword Bingo<br><br><strong>Challenge #3 - Spreading the Love:<br></strong>Team Member: [redacted]<br>Project: [redacted]<br><br><strong>Challenge #4 - Wildcard AND For the Best Use of Google Kubernetes Engine Autopilot:</strong><br>Team Member: [redacted]<br>Project: [redacted]<br><br>Congratulations to the challenge category winners and congratulations to all for creating such wonderful projects!! BeMyApp will be reaching out to the winning&nbsp;teams, as well as the winners of the survey participation drawing.<br></p><p>Hope to see you at a future hackathon!&nbsp;</p></blockquote><p>And here's the official&nbsp;<a href="https://cloud.google.com/blog/products/application-development/google-cloud-code_love_hack-winners" title="follow-up blog post">follow-up post</a>&nbsp;from the GCP blog.</p><p>So yeah, somehow, by the grace of all-that-be at Google, I won the gaming category. With 'only' 3 hours of work. </p><p>&#127881;&#127881;&#127881;</p><p>I can only assume this was possible because the judges were greatly amused by the idea of playing Buzzword Bingo at their next meeting. Or perhaps they just found the other entries to somehow be even&nbsp;<em>worse</em>. I don't know. I doubt they'd tell me even if I asked: the&nbsp;<a href="https://drive.google.com/file/d/1Ey0Ezn10tXEBMApJE8OjBs08lMA6uhR_/view" title="official rules">official rules</a>&nbsp;prohibit them from revealing the scores they gave for each project.</p><p>In any case, I'd say $2k USD for 3* hours of work is probably the best ROI I'll ever make, at least for a very long time.</p><p>And that's how I won a month-long hackathon in 3 hours. By reusing old work, making a big joke out of it, and not taking it seriously&nbsp;<em>at all</em>.</p><p>My hackathon win rate is now 100%.</p><p><em>* Obviously it took me more than 3 hours to originally build The Buzzword Bingo, but that&#8217;s not exactly the point, is it Mr. Pedant?</em></p><h1>Takeaways</h1><p>So what can you, dear reader, learn from all of this?&nbsp;</p><p>Whatever it is, I certainly don't suggest taking it to heart.&nbsp;</p><h1>Disclaimers</h1><p>There is nothing to disclaim except that there are no disclaimers. I wasn't paid to write this post or promote GCP in any way &#8212; I just thought the whole affair was funny.</p><h1>My Complete Submission</h1><p>And here's everything that I submitted as part of the hackathon. May it guide all of your future hackathon'ing efforts.</p><h2>Title</h2><p>&#8220;Lovely Buzzword Bingo&#8221;</p><h2>Short Description</h2><p>&#8220;A game to synergize with your corporate friends at the next holistic team meeting.&#8221;</p><h2>Banner Image</h2><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!w8bR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!w8bR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 424w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 848w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 1272w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!w8bR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png" width="1456" height="557" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/f9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:557,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:161957,&quot;alt&quot;:&quot;A screenshot of the Lovely Buzzword Bingo, showing a winning board.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A screenshot of the Lovely Buzzword Bingo, showing a winning board." title="A screenshot of the Lovely Buzzword Bingo, showing a winning board." srcset="https://substackcdn.com/image/fetch/$s_!w8bR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 424w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 848w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 1272w, https://substackcdn.com/image/fetch/$s_!w8bR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Ff9aa8fbe-92cd-4e38-b06c-7331243cd5f5_1919x734.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>Logo</h2><p></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cdvi!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cdvi!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 424w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 848w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 1272w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cdvi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png" width="128" height="128" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/a2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:128,&quot;width&quot;:128,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4145,&quot;alt&quot;:&quot;The logo for the Lovely Buzzword Bingo &#8212; the letters \&quot;L\&quot;, \&quot;B\&quot;, and \&quot;B\&quot; on a blue background.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The logo for the Lovely Buzzword Bingo &#8212; the letters &quot;L&quot;, &quot;B&quot;, and &quot;B&quot; on a blue background." title="The logo for the Lovely Buzzword Bingo &#8212; the letters &quot;L&quot;, &quot;B&quot;, and &quot;B&quot; on a blue background." srcset="https://substackcdn.com/image/fetch/$s_!cdvi!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 424w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 848w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 1272w, https://substackcdn.com/image/fetch/$s_!cdvi!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa2d7717b-569d-49ba-91df-83fb00c0e020_128x128.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p></p><h2>Video</h2><div id="youtube2-VV_pniw60eE" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;VV_pniw60eE&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/VV_pniw60eE?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><h2>"The Issue"</h2><p>&#8220;You know, sometimes, when I'm listening to my boss, or my boss' boss, ramble on and on about 'innovation', 'best practices', and 'blockchain', I just want to tune out. I'm sure all of my teammates want to as well, since we're all in the same boat really. So I wanted to build something to both increase our team synergy while improving the alignment between boss and team.&#8221;</p><h2>"Our Magic Solution"</h2><p>&#8220;Lovely Buzzword Bingo (<a href="http://lovelybuzzwordbingo.com/" title="">http://lovelybuzzwordbingo.com</a>) is our magic solution. Here, we have something fun and simple that _all_ team members can enjoy: bingo, but with buzzwords. Now we all _have_ to pay attention to what the boss says, otherwise how are we going to get a bingo? Everyone can just pull it up on their laptop, or another monitor, and play along. Truly, team bonding at its finest.&#8221;</p><h2>"How it Works"</h2><h3>1. Visit&nbsp;<a href="http://lovelybuzzwordbingo.com/" title="">http://lovelybuzzwordbingo.com</a></h3><p>&#8220;You'll be presented with a beautifully hand-crafted bingo board, with only the most holistic buzzwords included.&#8221;</p><h3>2. Attend a Company Meeting</h3><p>&#8220;Give the site to all your team mates as well. This is the point where you have to listen to your boss for once. You'll see, in the game, all the people who have connected.&#8221;</p><h3>3. Win!</h3><p>&#8220;Surely, if you were listening, then your boss will spout off enough buzzwords to easily get you a bingo. Congrats! You're a winner! Your random username will be displayed for all to see.&#8221;</p><h2>Slides</h2><p><a href="https://docs.google.com/presentation/d/1YQanInRIghocVIcVaRSlk0CxId4DtEtVsMyiyQNHfZE/edit?usp=sharing">https://docs.google.com/presentation/d/1YQanInRIghocVIcVaRSlk0CxId4DtEtVsMyiyQNHfZE/edit?usp=sharing</a></p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #10]]></title><description><![CDATA[On Track]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-10</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-10</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Mon, 14 Jun 2021 17:27:14 GMT</pubDate><content:encoded><![CDATA[<p>Today, on matters concerning my existence, is a short uFincs update. Nothing crazy going on at the moment, so I won't bore you with another multi-thousand-word post.</p><h1>Last Time</h1><p>During&nbsp;<a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-9" title="uFincs Update #9">uFincs Update #9</a>&nbsp;I went into a lengthy discussion trying to figure out what the next steps for uFincs were going to be. I eventually settled on switching out of marketing mode to get back into development mode. This way I'd be able to focus on building the next big feature: the import rules system.</p><p>That is the main subject of today's update.</p><h1>Import Rules System Progress</h1><p>At this point, the rules system is already all planned out and I'm deep into implementation work. As usual, the frontend work is taking the longest, although it makes extra sense this time around considering the grandiose scope of this feature (or at least, how I've envisioned it).</p><p>I had initially estimated this as being a 2-4 week adventure, but I think I'll revise that to 4-6 weeks if for no other reason than testing this whole thing is going to be a pain (lots of new moving parts).</p><p>Nothing to show off at the moment, considering all of the component work I've been doing has been in Storybook, but I'm sure there'll be something to play with by the next update.</p><h1>Other News</h1><p>One of the other things I mentioned in&nbsp;<a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-9" title="uFincs Update #9">uFincs Update #9</a>&nbsp;was that, although I wanted to switch off marketing mode, I still wanted to keep writing some content marketing-type blog posts to fill my downtime.</p><p>There is one post in particular that I've been working on for a good while now and it's turned into an absolute monster. The current working title is "How I (Re)designed uFincs" and it goes into the story of how I spent 2020 redesigning uFincs from the ground up. If you're interested in reading about the intricacies of UI/UX design, then you have this post to look forward to!</p><p>And with that tease, I have to unfortunately tell you that it's probably not coming out anytime soon. While it is still being edited at this point, it's one of those 'marketing' posts that I can use to really drum up awareness of uFincs. As such, it probably won't come out until my next marketing push, which probably won't be till much later in the summer.</p><p>And with that, I have fulfilled my non-contractual obligations to post a uFincs update.</p><p>Till next time!</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #9]]></title><description><![CDATA[Reflections on Launching, One Month In]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-9</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-9</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Thu, 20 May 2021 00:05:53 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!wMB4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Sigh... this is a <em>long</em> post. Longest post by a long shot. Enjoy at your own peril.</p><p>Anyways, if you haven't heard, I recently (hard) launched <a href="https://ufincs.com/" title="uFincs">uFincs</a> on <a href="https://news.ycombinator.com/item?id=27118220" title="Hacker News">Hacker News</a>. So guess what? The matter ever so concerning my existence today is the fallout of that event.</p><h1>Last Time</h1><p>The last time we talked about launching and marketing was during <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-7" title="uFincs Update #7">uFincs Update #7</a>, where I discussed the (good) fallout of initially launching uFincs, along with the unplanned Hacker News soft-launch.</p><p>It hasn't been long since I wrote that post, but now that we did our official "Show HN" post, I figured now's a good a time as any to reflect on where we are and think really, really hard about where we should be going. Basically, overthinking and overanalyzing all the many pieces of feedback I've received over the last month.</p><h1>The Hacker News Hard Launch</h1><p>For the unaware, I only refer to this event as a "hard" launch to differentiate it from the "soft" launch described in <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-7" title="uFincs Update #7">uFincs Update #7</a>, wherein I posted a comment on a popular Show HN thread that drove a good bit of traffic.</p><p>In this case, however, our "hard" launch was putting up a Show HN post of our own &#8212; specifically, <a href="https://news.ycombinator.com/item?id=27118220" title="this post">this post</a> on May 11th:</p><blockquote><p>Show HN: I built uFincs &#8211; a privacy-first, encrypted personal finance app</p></blockquote><p>As you might recall, I stated that, based on the reception of the soft launch, I expected this launch to either take off like a rocket or never turn into anything at all. As in, I didn't expect a lukewarm reception.</p><p>Well, I got a mostly lukewarm reception. Just over 50 upvotes on the main post is no mean feat (seemingly above average), but it is definitely not a rocket-level takeoff.</p><p>If we look at traffic numbers for the day, you'll see that they aren't even all that much better than the soft launch:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wMB4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wMB4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 424w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 848w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 1272w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wMB4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png" width="829" height="928" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:928,&quot;width&quot;:829,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:107749,&quot;alt&quot;:&quot;Web traffic for May 11th. It shows a spike around 1:00 PM, when the post started receiving traction, before dropping down to a steady level of traffic for the rest of the day.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Web traffic for May 11th. It shows a spike around 1:00 PM, when the post started receiving traction, before dropping down to a steady level of traffic for the rest of the day." title="Web traffic for May 11th. It shows a spike around 1:00 PM, when the post started receiving traction, before dropping down to a steady level of traffic for the rest of the day." srcset="https://substackcdn.com/image/fetch/$s_!wMB4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 424w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 848w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 1272w, https://substackcdn.com/image/fetch/$s_!wMB4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F05b65351-0961-4b9c-8649-ec2eb73c681d_829x928.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A couple hundred more visitors and page views is certainly good, but not exactly great. </p><p>However, to get a better view, let's look at the 5 day period following (and including) each launch. First, here's the soft launch (from April 28th to May 2nd):</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!qqlj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!qqlj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 424w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 848w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 1272w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!qqlj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png" width="852" height="570" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:570,&quot;width&quot;:852,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:34599,&quot;alt&quot;:&quot;Web traffic for soft launch. It shows a spike on launch day before plummeting down the following days.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Web traffic for soft launch. It shows a spike on launch day before plummeting down the following days." title="Web traffic for soft launch. It shows a spike on launch day before plummeting down the following days." srcset="https://substackcdn.com/image/fetch/$s_!qqlj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 424w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 848w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 1272w, https://substackcdn.com/image/fetch/$s_!qqlj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F65b5c365-940e-4c1b-9bb7-92faff1c2d2f_852x570.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And here's the hard launch (from May 11th to May 15th):</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!a-ud!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!a-ud!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 424w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 848w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 1272w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!a-ud!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png" width="834" height="573" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/23660383-7559-4891-8684-7004fc9f4296_834x573.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:573,&quot;width&quot;:834,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37502,&quot;alt&quot;:&quot;Web traffic for hard launch. While it also shows a spike on launch day, it also has a steadier decline in traffic over the following days.&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Web traffic for hard launch. While it also shows a spike on launch day, it also has a steadier decline in traffic over the following days." title="Web traffic for hard launch. While it also shows a spike on launch day, it also has a steadier decline in traffic over the following days." srcset="https://substackcdn.com/image/fetch/$s_!a-ud!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 424w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 848w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 1272w, https://substackcdn.com/image/fetch/$s_!a-ud!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F23660383-7559-4891-8684-7004fc9f4296_834x573.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Overall, we got roughly twice the visitors and page views for the hard launch. This would only seem reasonable considering the post would have had much larger visibility due to being on the front page of the Show HN section; we were in the top 5 for almost all of the first day and still within the top 10 throughout most of the second day. It's only once we fell down/off the Show HN page (on the third/fourth days) that traffic dropped off dramatically.</p><p>And if you're wondering, yes, the post did briefly make it to the front page of Hacker News, but only to #28 (i.e. the very bottom) for a very short period of time (less than 10 minutes). However, those 10 minutes on the front page easily represented several hundred visitors all on their own.</p><p>In terms of new customers... this launch has represented a gross gain of precisely 0 new customers (so far). I'll get into the overall conversion stats in the next section, but this certainly seems to be an indicator that either our lead times are just going to be longer than I might have hoped (in the best case) or it indicates that perhaps our traction was over-represented during the soft launch (in the worst case).</p><p>In <em>any</em> case, if we only look at traffic numbers, then I'd consider this launch to be a mild success. Not as good as it could have been, but as long as it gets uFincs in front of more people, then I guess anything's a success, right?</p><h1>Conversion Rates and Overall Stats</h1><p>Traffic stats are only one piece of the puzzle. Now let's narrow down the funnel and look at conversion rates. Specifically, sign-up and no-account login rates, as those are the two metrics that I consider the most important (and that I have the stats on).</p><blockquote><p>As a refresher, "no-account login" just refers to being able to try out the app without an account. It's basically our equivalent of a free trial.</p></blockquote><p>Let's start with overall sign-ups. To date, since we officially launched on LinkedIn on April 19th, we have had 29 people enter an email and password to create an account. Of those 29, 3 have become paying customers. So, 3/29 = 10.3% conversion rate when looking at sign-ups. However, at least two of those sign-ups used obviously fake emails, so discount those as you see fit.</p><p>Based on the <a href="https://www.smartkarrot.com/resources/blog/saas-conversion-rate/" title="first result from Google">first result from Google</a>, this conversion rate seems to be fairly average. However, since our business model is a bit different from those cited in the article (no free trial, but a 'free' no-account tier), who knows if it's actually any good.</p><p>If we choose to benchmark ourselves against the "freemium" model, then it'd make more sense to look at the no-account login conversion rate, so let's do that. Since our initial launch, we've had ~1600 no-account logins. At a 3/1600 = 0.1875% conversion rate, this is obviously utterly terrible, no matter how you benchmark it.</p><p>And if we look at our total traffic of 3.7k visitors since launching, then that means we can get roughly half of our visitors to try out the app in no-account mode, but obviously our sign-up and conversion rates look much worse from that angle.</p><p>However, one thing to keep in mind is that it is very hard for us to correlate all of these interactions to create a complete funnel of website visitor &#8594; no-account login &#8594; sign up &#8594; paying customer. This is simply a matter of not having the data to do it because of all the privacy-preserving measures we put in place. So all we can really look at are the raw numbers.</p><p>Additionally, an observation that I've made is that, so far, every sign-up that has turned into a paying customer has come from someone who signed up and then immediately subscribed to a plan. I've yet to observe anyone sign up, hit the paywall, leave, and come back later. I'll definitely have to email these prospects sometime in the future to ask if they're still interested (and tell them that their account will be deleted otherwise), so who knows if these parties will <em>ever</em> convert. Although it does make me question if we would get better results by  just following the industry standard of actually providing a free trial...</p><p>In any case, those are the overall stats that I'm looking at so far. Looking at them now, it definitely seems like we still have too little data to really base any hard decisions on (I mean, 3.7k total visitors certainly doesn't seem like a lot, absolutely <em>or</em> relatively speaking). As such, let's instead break down these stats to further compare the Hacker News launches.</p><h2>Stats for the Hacker News Launches</h2><p>Let's take a look at the raw stats for each launch, using the same 5 day periods that I used above for the traffic charts.</p><p>Soft launch:</p><ul><li><p>1.2k visitors</p></li><li><p>~1000 no-account logins</p><ul><li><p>1000/1200 = 83.3% no-account rate</p></li></ul></li><li><p>15 sign ups</p><ul><li><p>15/1200 = 1.25% sign up rate</p></li></ul></li><li><p>2 paying customers</p><ul><li><p>2/15 = 13.3% conversion rate</p></li></ul></li></ul><p>Hard launch:</p><ul><li><p>2.3k visitors</p></li><li><p>~500 no-account logins</p><ul><li><p>500/2300 = 21.7% no-account rate</p></li></ul></li><li><p>13 sign ups</p><ul><li><p>13/2300 = 0.56% sign up rate</p></li></ul></li><li><p>0 paying customers</p><ul><li><p>Big old 0% conversion rate</p></li></ul></li></ul><p>Right off the bat, it's obvious that Hacker News accounts for the vast majority of our traffic, period. However, it is curious that our no-account login rate was <em>so</em> much better for the soft launch over the hard launch (damn near 4x better) &#8212; nearly everyone who visited the marketing site also decided to try out the app. And obviously the paying conversion rate is infinitely better, so there's that.</p><p>I can only assume this is because the traffic from the soft launch was just so much higher quality (i.e. was greatly more targeted) than the hard launch. Because we hijacked a thread/comment that was very specifically about what uFincs is about (and because the thread itself had extremely high visibility), people who would read through to my comment seemed to be more inclined to read about and try out uFincs.</p><p>This is juxtaposed by the hard launch, where our traffic just turns into "ye olde generic Show HN browser". Sure, there was twice as much of it, but it certainly seemed lower quality (by these numbers, about 1/4 as 'quality').</p><p>However, I think that my messaging/branding played a good bit into this as well. During the soft launch, I purposely positioned uFincs as a compromise between pure privacy (dedicated apps/self-hosting) and pure non-privacy (a web app with a very permissive privacy policy). However, during the hard launch, because we specifically branded uFincs as "privacy-first", this seemed to 'trigger' a certain subset of folks into arguing that uFincs wasn't good enough privacy-wise (which, as the developer, I'm obviously aware of, but that's apparently just not good enough). I will get into the ramifications of this conclusion in a later section.</p><p>In any case, the numbers can only lead me to believe that the soft launch was actually more successful than the hard launch. This makes sense since it's in line with the old startup saying of "go where your audience is". While uFincs <em>might</em> have an appeal to the broader hacker audience, it <em>definitely</em> has an appeal to the more targeted "privacy-minded personal finance" audience.</p><p>So those are the hard (quantitative) numbers. Now let's take at the more qualitative data &#8212; specifically, all of the feedback and requests we&#8217;ve had lobbed at us.</p><h1>The Feedback</h1><p>As part of 'launching' (i.e. making posts to various platforms), I've received numerous pieces of feedback and requests, both in the form of comments and from people reaching out afterwards (mostly through email, cause I'm certainly not on any social media &#8212; unless you count LinkedIn...). I want to summarize all of that feedback here to put into context the decisions I'll be making later.</p><p>Let's start things off on a positive note.</p><h2>The Positive</h2><p>If there's been one consistent refrain from all the feedback I've received, it's this:</p><blockquote><p>The app design and execution are good/great/f*cking amazing.</p></blockquote><p>Even those with constructive/not-positive feedback to give praised the execution of the app and the marketing site. Everyone seemed to love it.</p><p>This is good, cause I spent I <em>significant</em> amount of time trying to get the design just right. I eventually want to write up a post on how I (re)designed uFincs, but let's just say that I always find UI/UX design to be the most time-consuming part of building out a feature.</p><p>In a distant second place, a large number of people liked/appreciated the straight-shooter/humorous spin that I put on the messaging for the marketing site. They indicated that it resonated well with them and found that it was fitting given the market (i.e. personal finance).</p><p>Finally, a good number of people appreciated the philosophy that I'm shooting for (privacy-first, encryption, having a one-time lifetime plan, being able to use the app without an account, etc). In fact, a good chunk of the people who reached out directly were those who shared my philosophies and wanted to see about potential collaboration efforts.</p><p>Overall, I'd say this is all pretty good. As they say in the startup world, "it's not about the ideas, it's about the execution", so being universally praised on the execution certainly seems like, if nothing else, a good start. Although I still have to wonder if it's enough...</p><p>Anyways, on to the more negative things (which will be kept separate from the feature requests).</p><h2>The Not-Positive</h2><p>There weren't many truly 'negative' things that I received as far as feedback goes. Really, whenever someone had something to complain about, it was mostly just requesting some form of a feature. However, we'll get into "feature requests" as part of the next section.</p><p>So what do I consider negative? Well, mindless complaints about the pricing are pretty negative. And also to be expected. I'm not exactly running a charity here, so getting people who complain about the pricing for an app that "just" forces you to manually record all of your transactions is well within the realm of reason. These complaints can basically just be ignored. If anything, when people complain that the monthly plan is 2x the price of the annual plan, that just gives me the incentive to raise the price of the annual plan, <em>not</em> lower the monthly plan!</p><p>The second big 'negative' feedback that I consistently received was partially negative feedback and partially feature request: that we weren't privacy-first "enough". In that, if we wanted to 'dare' call ourselves "privacy-first", we <em>needed</em> to be X. </p><p>To some people, X was "be open-source". To others, X was "being self-hostable" or "having dedicated mobile or desktop apps". Basically, since we're a web app, we can't possibly be "privacy-first" since we can just push "funny JS" at any time to compromise the security of everything and that it's all security theatre.</p><p>Which, from the strictest technical POV, <em>is</em> true. I've documented this well as part of our <a href="https://ufincs.com/policies/security#the-catch" title="Security Overview">Security Overview</a>. However, if we're coming at this from the <em>strictest</em> technical POV, then we also have to consider that there's nothing inherently more secure about being self-hostable (or a desktop/mobile app) &#8212; I could just as easily compromise the distributed executables or push a compromised update/executable sometime in the future. So there's still trust there.</p><p>OK, you say, then it must also be open-source. Well then, I say, are you <em>actually</em> going to audit all of the code? Or all of the dependencies? Are you even capable of determining whether something is secure even if you <em>could</em> audit it? Unlikely. So you either trust that someone else has audited the code, or you just loop back to trusting that the code is 'inherently' secure (because I say it is). So there's still trust there.</p><p>So really, what these people are complaining about is less that we are not "privacy-first enough"; it's that we aren't a 100% trust-less solution (and &#8212; spoiler alert &#8212; nothing is). So the fact that we are a web app has &#8212; again, in the strictest sense &#8212; nothing to do with it. Sure, it makes it slightly <em>easier</em> for us to compromise things, but there's still trust involved one way or the other. </p><p>Of course, Hacker News (and the programming community at large) has a known bias for 'hating' on JavaScript and web apps in general, so complaints like this are to be expected (I didn't write up that Security doc without an audience in mind, after all). Whether or not they should be taken seriously is a different matter entirely.</p><p>Other than that, there wasn't much 'truly' negative feedback.</p><p>So now let's take at the 'mixed' feedback: aka all of the wonderful features that uFincs supposedly needs.</p><h2>The Requests</h2><p>While there was a good amount of positive feedback and a small amount of negative feedback, there were a <em>lot</em> of feature requests. A good chunk of it came in the form of features we're definitely not supporting (specifically, bank integrations), but I was surprised that no one asked for anything budgeting related.</p><p>However, there were certainly a non-zero number of people who stated things along the lines of "if only uFincs had X, I'd pay up immediately!" or "uFincs would be a hit if only it had Y!".</p><p>Mmhmm, right.</p><p>Before we get into the nitty-gritty of what features were actually requested, let me first address the chicken and the egg problem that I face as a developer/business owner: I want to prioritize the feature requests of customers who have already paid (i.e. I want to provide good customer support), but people don't want to pay until uFincs has all the features they want. So, how do I prioritize which features to work on? This is a question that we will dive <em>deep</em> into in a later section, but keep it in the back of your mind for now.</p><h3>Desktop/Mobile Apps</h3><p>By far, one of the most requested 'features' was to have a dedicated desktop and/or mobile app. The reasons for <em>why</em>, however, deferred a good bit. </p><p>Some people wanted a mobile app so that they could use uFincs on the go (but they already can with the mobile version of the web app? which is, by the way, also an offline-first PWA). </p><p>Some people wanted dedicated apps for the 'security' improvements (which, as we addressed above, are 'just' security theatre). </p><p>And others merely stated that it'd be "good" to have. Very helpful. </p><p>Thankfully, it doesn't take much to convince me that dedicated apps are a good idea &#8212; they're already on my roadmap for a reason!</p><h3>Open Source and API</h3><p>Next up on the 'most requested' list was making uFincs open-source. The most obvious reason was for greater transparency (we say we're encrypted but are we actually?), although there was certainly a good number of people who just wanted it open-source otherwise. Surely this wasn't just code for "I want it open-source so that it would be free", so I'll definitely assume otherwise. </p><p>Open sourcing is kind of a tricky balancing act when it comes to SaaS. It's basically making the bet that the goodwill generated by open sourcing will be enough to bring in new customers while still being greater than the support load that comes with maintaining an open-source product/community. And if there's one thing I've learned during all of my startup research, it's that free customers are the worst (primarily because they tend to be the most support sucking). However, the fact that we bill ourselves as "privacy-first" does change the equation just a <em>tiny</em> bit. </p><p>Something interesting that someone suggested as the reason that people want uFincs open-source is that they actually want some way to extend uFincs &#8212; aka, an API. Unfortunately, making a public API for uFincs is quite a bit more tricky because of all the encryption we do client-side. As such, we'd basically need to provide a programmable client of some sort rather than just a REST API (e.g. a sort of uFincs SDK, or perhaps a kind of self-hostable API proxy).</p><p>In either case, open-sourcing and building a public API of some sort are definitely things that I want to do (as a developer), it's just a matter of weighing them against their business benefits.</p><h3>Multi-Currency Support</h3><p>Currently, uFincs supports nothing but the glorious dollar sign $$$. As a Canadian, this makes nothing but perfect sense.</p><p>Obviously, there are other countries in this world. Multi-currency support is just kind of one of those "some time, in the future" features that I've kept putting off. Although, there are different ways this kind of support can be implemented, at its most basic level, we could just change which symbol is displayed next to monetary symbols. Easy enough. </p><p>But, as one person requested, there's also a need for being able to automatically convert between different currencies. This increases the complexity by a <em>fairly</em> large amount, due to now requiring the use of external APIs for dealing with the exchange rates, as well as a rather large overhaul of the accounting system (as it was definitely not designed with this in mind). But I can see why such a feature would be useful.</p><p>In any case, this was one of the most 'concrete' features requested.</p><h3>Better Import System</h3><p>Oh boy. This was a contentious topic (for a good reason).</p><p>As it turns out, there are a large number of people who <em>don't</em> enjoy entering each of their transactions by hand (shocking, I know). Even more shocking, there are people with a long history of transactions that they don't want to just ignore/throw away! Who'd have thought?! (certainly not me)</p><p>Well, that's why we built CSV importing to begin with, but apparently that's just not good enough for some people. For some people, who have thousands of transactions in a giant CSV file, the prospect of going through and manually categorizing each transaction to get it into uFincs is just too much. Hard to blame them. For these people, a sort of 'rules' system for automatically assigning accounts based on descriptions seems to be the most fitting solution.</p><p>But others, they don't have CSV files &#8212; they come from other programs like Ledger CLI or (gasp) GnuCash itself. They want to just import their stuff over immediately, without going through an intermediary.</p><p>And finally, the most dreaded group: the ones who want bank integrations. They don't want to deal with importing or manually entering things &#8212; no, they want it all done automatically. As I wrote in "<a href="https://blog.ufincs.com/p/why-categorizing-upfront-is-more" title="Why Categorizing Upfront is More Effective">Why Categorizing Upfront is More Effective</a>", that whole flow runs completely opposite to the ideals of uFincs (aka my ideals). After all, when I envisioned uFincs, there were two things I said I'd never do: bank integrations and budgets.</p><p>However, from a business perspective, it's perhaps not prudent to ignore these needs (but maybe it's also not prudent to spread myself too thin). </p><p>In any case, while I still don't want to support dealing with things like Plaid, I do believe I have a potential solution to 'the bank problem': if/when we finally create a public API (of some sort), then people can just integrate with whatever they want. Plaid has a free tier, so the community could create their own Plaid integrations and everyone would be happy! (in theory)</p><p>But I think the main takeaway is that, as much as I want to push "recording manually is better", that's definitely a smaller market than those who want automation (which I expected; I had just hoped it wouldn't be <em>too</em> small).</p><h2>The Biases</h2><p>While all of these requests and feedback is good, it's also good to keep in mind the biases of who's providing the feedback (and who's not).</p><p>Obviously, the groups we've targeted thus far have been primarily of the developer/entrepreneur mindset. Being myself of those mindsets, I feel I can understand fairly well where people are coming from when they give positive/negative feedback.</p><p>What's worth noting is that I feel it can be misleading to work off of only given feedback. Just because someone left a comment somewhere doesn't mean that we should necessarily value it highly. What about all the people who <em>didn't</em> leave comments? Hard to work off anything from them, for obvious reasons, but the <a href="https://en.wikipedia.org/wiki/1%25_rule_(Internet_culture)" title="1% rule of Internet culture">1% rule of Internet culture</a> is a thing for a reason. </p><p>Additionally, we also have to take into account the feedback of our existing customers... except we've gotten next to no feedback from them. This either indicates that they're happy with the current experience, or confident that, whatever might be 'wrong', I will be able to notice and fix it (alternatively, if they're privacy-minded, they just don't want to talk to me).</p><p>Finally, the way I worded my launch posts can certainly lead to biases in the feedback I received. Taking the Hacker News hard launch post as an example, because I made sure to link to a FAQ and specifically address things like pricing, there were next to no comments on the things that were addressed. Does that mean that everything is good? No, it just means that people were disincentivized from talking about the things I addressed in favour of talking about things I <em>didn't</em> address (case-in-point, self-hosting).</p><p>In retrospect, perhaps it was to my detriment to try and minimize repeated discussion about things I've already addressed (by providing the FAQ) since it gives me less qualitative data to work off of.</p><h1>Analyzing the Feedback</h1><p>We've already gone through a bit of the analysis w.r.t. the feedback and feature requests, but I want to go up a level. Think things through at a more aggregate level.</p><p>First, the most important thing to realize is that our feature requests can be broadly grouped into two categories: product-level and ecosystem-level. </p><p>Multi-currency support, a better import system, bank integration? Those are all product features. Open-source, desktop/mobile apps, an API? That's right, ecosystem features.</p><p>Totally coincidentally, these two categories also map fairly well to the two sides of the audience that we target: people who want a good-looking, full-featured app (aka the "modern GnuCash" side) and people who care about privacy. Our original goal was to build something that would target people who share traits from both audiences; that is, our 'niche' would be "privacy-first finance apps that use double-entry accounting". </p><p>Based on the data presented so far, however, it would appear that we are not adequately satisfying either audience. The 'features' group finds uFincs lacking in, well, features, and the 'privacy' group is much the same. This means that either our mythical 'blended' audience is too small or we just haven't gotten uFincs in front of enough people. At sub-10,000 people that even know about uFincs' existence, it could very well be the latter, but the former is always what keeps me up at night.</p><p>Something worth noting is the relative sizes of these audiences, at least from an intuitive POV. It is (quite) likely that the audience of people just looking for a good, solid personal finance app is much, much larger than the audience of people who particularly care about privacy in general. As such, one might even consider the former audience to be a so-called "mass market". </p><p>Some feedback that I've received (throughout the entire process of building uFincs) is that we should be targeting a more mass-market audience; some have even gone so far as to describe uFincs as being "too sophisticated", primarily taking issue with my dedication to the art of double-entry accounting. </p><p>However, my sheer stubbornness has caused me to continue building an app that, first and foremost, meets my own needs. It's just been that, as a startup, our greatest "leap of faith assumption" is that there others like me who want a similar app. While finding 3 such people within the first couple of weeks of launching seemed to indicate potential traction or product-market fit, perhaps it was much more of a fluke than I initially thought.</p><p>In any case, I would like to posit a couple of possible scenarios. The following is that "later section" that I kept mentioning above. These scenarios break our current leap-of-faith assumption: instead of trying to appease a mixed audience, what if we catered to only one or the other?</p><p>That is, what would happen if we went down the path of pleasing the hard-line privacy advocates? What would happen if tried pleasing only the 'features' crowd? </p><p>All of this is to try and determine our path forward from here.</p><h2>Diving into Privacy-First</h2><p>As we've established, we currently call ourselves "privacy-first", but some people think that that's too strong of a term to be using at this stage.</p><p>Well, what would need to happen to fully satisfy this group of people? What would we need to do to be 'truly' "privacy-first"?</p><p>I would think that the first thing would be having to open-source all of the code. The immediate side-effect of this is that the "non-negotiable" trait of "privacy-first" &#8212; being self-hostable &#8212; would then be quickly checked off. After all, with all the code comes all of our Docker configurations, and it's not like uFincs is a particularly difficult app to host.</p><p>Personally, I would assume that this would be sufficient for 80% of hard-liners. They could inspect the code (or not), they could run the code themselves, they could be 'reasonably' assured that no funny business is going on and be content using the product (assuming they weren't also partly a 'features' person, which, let's face it, everyone is).</p><p>That last 20% would likely still be too skeptical of running a web app (even if it was self-hosted!) and would demand a mobile and/or desktop app.</p><p>Hey, I never said these theoretical hard-liners were rational.</p><h3>The Costs</h3><p>Now, what are the costs of the above? Well, open-sourcing is &#8212; in the strictest sense &#8212; trivial to do. Self-hosting, if we didn't explicitly support it but merely allowed it through the ability to build your own Docker images, would also be trivial. Although if we did explicitly support self-hosting, then there's the matter of maintaining a public repository of the images as well as having to deal with customer support. Of course, open sourcing would also bring on an amount of 'customer' support (since community management doesn't exactly come free). </p><p>The desktop app would, technically, be easy to put together because we'd just be wrapping the app in Electron. But there are more costs to it than just 'building' it. Just like the Docker images, a desktop would app would still require dealing with distribution and updates. Also, there are the very real (monetary) costs of having to buy into the Mac/Windows ecosystems to handle just compiling the app for each environment (although this is more true for Mac than Windows). </p><p>Finally, the mobile app would be the most costly, both in terms of time and money. Money, again, because of buying into the Apple ecosystem, and time because of having to build the app itself. </p><p>While I <em>could</em> wrap the existing web app up with something like <a href="https://www.pwabuilder.com/" title="PWABuilder">PWABuilder</a> to make a 'native' app, you can only do that for Android. So, if we wanted an iOS app, the least costly approach would be to build it using React Native. Thankfully, I've made sure to build uFincs with the assumption that large parts of it would eventually be re-used in a React Native app, so a good portion of my work would be re-usable. But even just the work of rebuilding the UI is non-trivial, so I would expect a solid several months would be required to put together a proper mobile solution. </p><p>Tack on the usual support and distribution costs from everything else above, plus the fact that we now have to maintain a completely separate app (on two platforms!) and the costs are quite high.</p><h3>The Benefits</h3><p>Those are the high-level costs to diving into the privacy-first audience, assuming that audience <em>only</em> cared about privacy. But what about the benefits? What do we gain from pleasing this particular group of people?</p><p>...</p><p>...</p><p>Good will?</p><p>...</p><p>Realistically, from a monetary POV, I wouldn't expect to generate much of anything from this audience. How exactly do we generate money from these alternative software distribution models? Charge for prebuilt versions of the self-hostable images or desktop/mobile apps? Well, when there seems to be a large overlap between "cares about privacy" and "knows things about technology", this seems unlikely to be viable (people will just compile from source).</p><p>"What about asking for donations?", you say. To which I reply: we all know how poorly that works out in practice for the majority of software projects.</p><p>As such, none of these alternative models seem nearly as viable as SaaS, so the expected (direct) ROI is basically zilch. At best, the good will generated by these users would prompt other not-so-privacy-caring folks to opt for the convenience of the SaaS and pay for that instead. </p><p>You might say, "Wait a minute,  you're discounting all of the 'free' dev work that you'd get from people wanting to contribute!", and you're right: I am discounting it. To zero.</p><p>Altogether, <em>this</em> is the ramification of trying to cater too hard to the privacy side of the equation: it just doesn't seem viable as a small startup. Especially a small startup with only a single person, that's bootstrapping, is in the personal finance sector, and deals in B2C SaaS. I mean, that's <em>already</em> a cocktail for things to go wrong; no need to throw the friggin lighter at it!</p><p>Obviously, other companies seem to be able to get away with this business model ('privacy-first', open-core, and SaaS) &#8212; take <a href="https://plausible.io/" title="Plausible Analytics">Plausible Analytics</a> for example. But I think one key differentiator is that these companies tend to be B2B. Businesses want the support and convenience of just paying for a SaaS rather than the overhead of self-hosting, so it makes good business sense to just pay up (plus businesses tend to pay more in general). </p><p>I can't even think of any B2C companies pulling this off!</p><h3>Thoughts</h3><p>So yeah, doesn't seem like focusing solely on the privacy-first aspect is our way forward. Instead, maybe we should treat this is as a marketing/branding problem instead of a development one. It's much easier to slightly re-brand ourselves from "privacy-first" to the (apparently much less strict) "privacy-friendly" rather than catering to all the needs of this audience, so maybe we'll just do that.</p><p>This isn't to say that I don't want to (eventually) do all of the above. Remember, this discussion is more of a matter of determining our next immediate steps.</p><p>Let's now consider the opposing scenario of dealing only with the 'features' crowd.</p><h2>Diving into Features</h2><p>Let's say we ignored the privacy crowd for now. What would we do instead?</p><p>Obviously, build out features. Which features? Well, considering I've received 'commitments' on the order of "if uFincs had X, I'd pay" for both multi-currency support <em>and</em> a rules system for importing, I'd say those seem like pretty decent places to start.</p><p>Beyond that, bank integration seemed to be the #1 missing feature for the more mass-market developer, but since I'm adamant about not (directly) having it, I figure opening up API access would be a viable alternative. If we were to just expose the existing API as is, that would either mean shunting all of the encryption work off to the downstream developers, which would likely end <em>very</em> poorly, or just throwing out encryption altogether so that the API only deals with regular data. The later would certainly be the easiest (from a downstream developer's POV), but I don't think there's much sense in throwing out all of that work.</p><p>That means that 'exposing the API' would really come in the form of something like a Node client that can access our API while still preserving the privacy aspects of our encryption system.</p><p>Of course, not everyone wants to work in JavaScript/Node, but since I also don't want to support a client for every language under the sun, the better solution would be to have some sort of API proxy that is self-hostable separate from the app itself. That way, users could self-host an API server that handles the data encryption/decryption/key management while still being able to interact with uFincs via the ubiquitous interface that is REST. I think this combination of Node client + API proxy would be particularly potent.</p><p>Coincidentally, the Node client and API proxy would also be good targets for open-sourcing if only because they'd be much smaller to deal with. Plus it'd give me another (good) excuse to push forward open sourcing our custom e2e encryption Redux middleware since that's what currently encapsulates all of our encryption logic. So even the privacy folk win a bit here!</p><p>Speaking of winning...</p><h3>The Costs</h3><p>As opposed to the general 'ease' of just throwing all of the code up on GitHub and calling it a day, these features have a definite time cost to them. </p><p>For multi-currency support, eesh, I'd say that's easily a month-plus endeavour. Between all of the new UI work, having to re-architect a good portion of the system, and bringing in third-party APIs, that's a very significant feature to deal with. </p><p>For an import rules system, depending on how fancy I get with it, I can see that being a 2-4 week endeavour. That, again, is mostly a UI problem; the data structures and logic aren't anything special. But since I'm so slow and deliberate in making sure the UI/UX is good, that's what ends up taking the most time.</p><p>As for a basic Node client to replace API access, that's actually a lot harder to estimate, if only because it's more of an unknown than the other features. In theory, it's just "pass some data, encrypt it, send it off to our API" or "request some data, decrypt it, pass it to the developer", but I think there's going to be a good bit more work that goes into the API design than anything else. Thankfully, we can re-use most (if not all) of the encryption/decryption logic, so that won't be much of a problem. Just to design and build the API is likely a sub-month-long endeavour, but adding in all of the necessary testing and documentation that goes along with something of this class could definitely push it past a month.</p><p>If we wanted to be 'super' extra, we could write our own Plaid integration using the Node client, both as an example and both so that users get some immediate benefit out of it. Even better if we can combine this with our rules system to automatically classify transactions! I figure that wouldn't take more than a week or two.</p><h3>The Benefits</h3><p>Well, I guess we kind of already went over the benefits. If we built out everything, then I have at least a handful of people I can go to and say "look, your request has been built; you ready to pay now?". Obvious monetary benefit #1. </p><p>A good side effect of this is that these people would hopefully then be enthralled by the fact that I listened to and implemented their feedback, so maybe word-of-mouth then comes into play.</p><p>Besides these features directly benefiting those who requested them, they also broaden our market appeal. That means that subsequent marketing pushes should appeal to the larger "personal finance" audience in general. Of course, the downside to this is that we start becoming that much more similar to our competitors, weakening our niche. Although considering my concerns about our niche being <em>too</em> niche, maybe that's not a bad thing.</p><p>And like I said, there's no reason to throw out all of the privacy-related work we've done so far; it just means softening the messaging.</p><h1>So Where do We Go From Here?</h1><p>So after all that (over) analyzing, where the heck do we go from here?</p><p>Well, first we need to re-iterate what our goals are. Currently, the most pressing goal is to acquire customers, make money, and reach ramen profitability. If we fail to hit our yearly goal of 100 customers, then... bad things.</p><p>With that in mind, what are the actions we could take? I think it basically comes down to a series of classic "pivot or persist" decisions:</p><ul><li><p>Do we continue on with marketing, trying to acquire more customers? Or do we switch back to product development, to increase the chances of future marketing pushes being more effective?</p></li><li><p>If we do switch back to development, what do we work on? Do we tackle feature requests or ecosystem requests?</p></li></ul><p>Let's start with the first question.</p><h2>Marketing or Development</h2><p>As of the writing of this post, it has been almost exactly one month since we first launched. Between all of the blog writing, the launching, replying to people, and not doing much of any proper development work... I'm <em>exhausted</em>. I feel like just sinking my teeth into a large new feature just to change things up &#8212; even if, from a business POV, I'd probably be better served by continuing this marketing push.</p><p>However, I think we can compromise on this by instead just down-sizing our marketing efforts back to writing blog posts. There are a couple posts that I'm particularly keen on writing, so I think the best choice (at this very moment) is to switch back into development, work on something big and new, and fill out my downtime with some blog post writing. Of course, in the short term, making that minor branding switch from "privacy-first" to "privacy-friendly" should also be done.</p><p>Such are the tradeoffs you make when you are but one person: can't exactly do everything at once. And even if you tried, that's a speedrun to burnout.</p><h2>Features or Ecosystem</h2><p>Based on our analysis of the feedback, I think it's obvious that we should focus on more features right now. Particularly, more ways of letting users get their data into uFincs. The obvious starting point for this is the import rules system, followed by the Node client for enabling 'API' access. I think those two features will open up our audience options by quite a bit, while still not compromising on our core privacy values.</p><p>And maybe, just to appease the many other people of the world, we can throw in a preference for changing the dollar sign (but not yet implement full multi-currency support).</p><p>That should cost us somewhere in the neighbourhood of a couple months of work, after which we'll be able to either pivot back to marketing or maybe start pushing towards desktop/mobile apps.</p><h1>Wrap Up</h1><p>Given an endless number of requests, features, and possibilities, how can one possibly determine which option is the best at any point in time? Well, thus far, I've used the heuristic of "whatever <em>I</em> would want". But now that we've received real people requesting specific features, it would seem that changing that heuristic is in our best (business) interests.</p><p>So while our hard-launch on Hacker News might not have been the most successful (in the short term), I think, for being only a month in, we're doing OK. As they say in YC: "Talk to your users, write code". </p><p>I guess it's time to get back to writing code.</p><p>Till next time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #8]]></title><description><![CDATA[DevOps Detour - Part 3]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-8</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-8</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Sun, 09 May 2021 16:58:44 GMT</pubDate><enclosure url="https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/2441320a-3cc3-4bcd-9a72-91e0ae3029e8_150x150.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's time for round 3 of the DevOps Detour! The only matter that concerned my existence several weeks ago and that you're only learning about through these chopped-up blog posts!</p><h1>Last Time</h1><p>The <a href="https://www.onmattersconcerningmyexistence.com/p/ufincs-update-6" title="last part">last part</a> was all about cluster upgrades and improvements. And guess what? We got more of the same today!</p><p>Only this time, we're gonna tackle upgrading some in-cluster services that have been out-of-date for far too long. Get ready for some hot, steamy, <em>debugging</em> action!</p><h1>Upgrading cert-manager</h1><p>Oh <code>cert-manager</code>, how I loathe thee.</p><p>Or at least, I did before realizing how easy the upgrade would be.</p><p>See, just like everything else in the cluster up to this point, it was kind of a "set it and forget it" affair. Couple of years back, <code>cert-manager</code> was still fairly new on the block. But since it was replacing a similar but deprecated project (<code>kube-lego</code>, I believe), I figured I just had to bet that it would be 'the' solution for.. cert management in Kubernetes.</p><p>Thank god I was right, cause I really did not want to deal with setting up automated TLS certs more than I had to.</p><p>In the beginning, most of the pain of <code>cert-manager</code> came from its infuriatingly vague error/log messages. This, combined with having to wait X hours for DNS changes to 'propagate', made debugging and getting the first set of TLS certs issued a <em>royal pain in the ass</em>.</p><p>And then there was that time that <code>cert-manager </code><em>had</em> to be upgraded because of a <a href="https://www.reddit.com/r/kubernetes/comments/cxcyxq/upgrade_cert_manager_asap_if_not_already/" title="bug">bug</a> that was causing it to ping Let's Encrypt too often...</p><p>And then there was the time they introduced the webhook service and it <a href="https://www.revsys.com/tidbits/jetstackcert-manager-gke-private-clusters/" title="broke">broke</a> <code>cert-manager</code> in GKE unless you opened a very specific port to the control plane...</p><p>And then there was that time where deleting the <code>cert-manager</code> namespace would just <a href="https://github.com/jetstack/cert-manager/issues/1399" title="never finish">never finish</a> and be stuck in limbo...</p><p>Yeah, it wasn't fun in the early days.</p><p>Eventually, things (mostly) stabilized when it hit v0.12. There was still the odd hiccup where <code>cert-manager</code> would just fail to renew a cert (one time it was because the GCP service account expired... another time it was seemingly because of a cosmic bit flip...), but it (mostly) worked.</p><p>So when I read the news that they had <em>finally</em> released a v1.0 last year, I have dreaded going through with the upgrade process ever since.</p><p>Well, with all these other upgrades and improvements I was making, I figured I might as well bite the bullet and upgrade to (what I could only hope) is an actual stable version of <code>cert-manager</code>.</p><p>&#8230;</p><p>My god. It went splendidly. Damn near perfectly, really (and for those who know me, no, that was <em>not</em> sarcasm).</p><p>First of all, <code>cert-manager</code> has <em>excellently</em> documented all of the upgrade steps needed to move between versions. For example, here are the instructions for <a href="https://cert-manager.io/docs/installation/upgrading/upgrading-0.12-0.13/" title="v0.12 to v0.13">v0.12 to v0.13</a>. Rather empty, which is a good thing!</p><p>In fact, moving all the way from v0.12 to the latest v1.3 (a jump that included 8 separate releases!), there was only <em>one</em> upgrade step I needed to do: from <a href="https://cert-manager.io/docs/installation/upgrading/upgrading-0.13-0.14/" title="v0.13 to v0.14">v0.13 to v0.14</a>, you just needed to delete the deployments before applying the new ones.</p><p>So I literally just deleted the <code>cert-manager</code> deployments and applied the latest v1.3 manifests.</p><p>Worked without any other changes.</p><p>Honestly, I was flabbergasted. I literally set aside like 6 hours to do the upgrade (expecting everything to go wrong), but nothing did. Certs still worked, got renewed, no errors. Just, magic.</p><p>So... huge props to <code>cert-manager</code> for finally getting things sorted out!</p><p>Well, except for one thing...</p><h2>CA Injector Errors</h2><p><code>cert-manager</code> runs three different deployments by default: the main <code>cert-manager</code> process that handles issuing and renewing certs, the <code>webhook</code> process that ensures the manifests we deploy are configured correctly, and the <code>cainjector</code> process that... presumably handles injecting a.. CA.</p><p>It doesn't really matter <em>what</em> the <code>cainjector</code> was doing, the point is that it was throwing a whole heck of a lot of errors. Like, hundreds of errors an hour.</p><p>Specifically, errors with the message <code>"unable to fetch certificate that owns the secret"</code>, <code>"Certificate.cert-manager.io \"[redacted]\" not found"</code> (GitHub issue over <a href="https://github.com/jetstack/cert-manager/issues/1489" title="here">here</a>).</p><p>Basically, what seemed to be happening is that the CA injector would try to.. inject the CA into secrets that it couldn't find the corresponding <code>Certificate</code> resource for. Now, why wouldn't it be able to find the <code>Certificate</code>? Well, I'm not quite sure, but I have a good hunch...</p><p>See, as totally awesome as <code>cert-manager</code> is, it had (and still has!) this weird idiosyncrasy: when it issues a cert, it does so in a Kubernetes <code>Secret</code> resource. In order to make use of this cert, we need to give it to our <code>Ingress</code> resources so that <code>ingress-nginx</code> knows which cert to use for TLS.</p><p>However, because of our per-branch deployment scheme, our <code>Ingress</code> resources are spread across many different namespaces. But the cert <code>Secret</code> is only in the <code>cert-manager</code> namespace... and the <code>Ingress</code> can only read cert secrets from its own namespace...</p><p>So how can we use the cert? Well, based on my understanding at the time (which seems to still hold <a href="https://cert-manager.io/docs/faq/kubed/" title="today">today</a>), you're supposed to just copy the secret between namespaces.</p><p>A little brute force, but hey it works. Especially when you throw in the <a href="https://github.com/boxboat/kube-ingress-lets-encrypt/blob/master/ingress-cert-reflector.yml" title="ingress-cert-reflector">ingress-cert-reflector</a> to handle automatically copying the secret to every namespace.</p><p>This worked fine for quite a while. Until I finally decided to check the logs and noticed the <code>cainjector</code> complaining about every secret in every namespace <em>it</em> wasn't in. Presumably because there was no corresponding <code>Certificate</code> resource in each namespace that the secret was copied to.</p><p>But of course, since my HTTPS was working just fine, I didn't really care all that much, so I just let it be.</p><p>But when I finally upgraded <code>cert-manager</code> and found that the <code>cainjector</code> was <em>still</em> throwing these errors, I figured I might as well try and fix it properly.</p><p>And that's when I finally learned that <code>ingress-nginx</code> has a <a href="https://kubernetes.github.io/ingress-nginx/user-guide/tls/#default-ssl-certificate" title="--default-ssl-certificate">--default-ssl-certificate</a> option.</p><p>I have no idea if this option existed back when I first set everything up, but if it did, I regret not stumbling onto it sooner. </p><p>As the name suggests, it allows you to specify a default SSL cert by pointing at a secret in <em>any</em> namespace. WOW. How crazy is that!?!</p><p>So <em>bam</em>, just like that, I no longer need the <code>ingress-cert-reflector</code> to copy my cert secrets around. Just specify a default and we're all good.</p><p>There's just one problem... you know how everything up till now has been "one thing to leads to another"? Well, this also led to another thing.</p><p>I figured, heck, while I'm over here re-configuring <code>ingress-nginx</code> and upgrading <code>cert-manager</code>, why don't I <em>also</em> upgrade <code>ingress-nginx</code>?!?</p><h1>Upgrading ingress-nginx</h1><p>You remember those 6 hours I had allocated for the <code>cert-manager</code> upgrade? This is where they went.</p><p>At the time, I was running <code>ingress-nginx</code> v0.17.1&#8230;</p><p>The latest version was <strong>v0.45.0</strong>. Only, like, a couple <em>dozen</em> versions to jump up. No biggy right? If <code>cert-manager</code> went smoothly, <em>surely</em> so would <code>ingress-nginx</code>?</p><p>Nope, no it would not. I mean, it could have been <em>worse</em> (what can't be?), but this upgrade wasn't fun.</p><p>First of all, the <a href="https://kubernetes.github.io/ingress-nginx/deploy/upgrade/" title="upgrade docs">upgrade docs</a> were <em>much</em> worse than <code>cert-manager</code>'s:</p><blockquote><p>To upgrade your ingress-nginx installation, it should be enough to change the version of the image in the controller Deployment.</p></blockquote><p>"Should be enough", yeah right!</p><p>First of all, at some point in the past, <code>ingress-nginx</code> changed where they hosted their Docker images from Quay to GCR. So... I literally can't just change the version because I'm on an old enough version that I used the Quay image, and they don't publish the latest images to Quay anymore! Strike #1.</p><p>Secondly, since I use the static manifests, I can just check that there are fairly significant changes between my (old) version's and the latest's. So... changing 'just' the image, not quite. Strike #2.</p><p>Then again, it's hard to blame them for not having upgrade docs for such an old version. It's more commendable that <code>cert-manager </code><em>does</em>.</p><p>Anyways, I figured the easiest way to upgrade is to just grab the latest manifests and YOLO it. Obviously, I ported over whatever config changes I had made, but other than that, I literally just YOLO'd it.</p><p>And.... it worked! Or at least, it seemed to work. Containers weren't throwing any errors. Site was coming up just fine. </p><p>Except then I tested this one thing... I navigated directly to <a href="https://app.ufincs.com" title="app.ufincs.com">app.ufincs.com</a> (the subdomain for the app itself, vs <a href="https://ufincs.com" title="ufincs.com">ufincs.com</a> which is for the marketing site) and instead of being redirected to the login page, I found something strange: I was presented with the home page of <a href="https://ufincs.com" title="ufincs.com">ufincs.com</a> instead.</p><p>Uh oh.</p><p>Then I tried navigating to the login page via the Login link on this strange home page. Worked.</p><p>But then I tried navigating directly to the login page via <a href="https://ufincs.com/login" title="ufincs.com/login">ufincs.com/login</a>: infinite request loop.</p><p>Oh shit.</p><p>First thing I did was roll back the <code>ingress-nginx</code> upgrade to make sure this wasn't somehow already how it worked. It <em>shouldn't</em> have been, but since the marketing site does use redirects to get the user to the app, I figured it's <em>possible</em> that I had somehow broken it earlier.</p><p>But it was fine with the old <code>ingress-nginx</code>. So it's definitely the new version's fault.</p><p>But what could it be? What could have changed between versions of <em>Nginx</em> to cause this strange behaviour?</p><p>My first thought? Caching. </p><p>See, we make use of Nginx to cache the static assets that are served from the Express servers of the marketing/app services. Nginx is <em>way</em> faster at serving that content than forcing the Express servers to do it all (and I didn't want to bother setting up a proper CDN).</p><p>So somehow, the root route for the marketing site was being cached and served for the app service. And then... <em>something</em> was being cached incorrectly to cause the infinite login request loop.</p><p>After some <code>curl</code> debugging to check the cache hit/miss status of various pages, flushing the Nginx cache, trying to <em>inspect</em> the Nginx cache, and finally just setting up separate caches for the marketing and app services, I deduced that it was indeed a caching issue. The only question was <em>why</em>.</p><p>My assumption was that, somehow, the key used to index cached requests had changed between Nginx versions. Digging into the  <a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html" title="settings">proxy_cache settings</a>, I found the default value for the <a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_key" title="proxy_cache_key">proxy_cache_key</a>:</p><pre><code>$scheme$proxy_host$request_uri</code></pre><p>Well, the <code>$scheme</code> should be the same between requests; it's just HTTPS. The <code>$request_uri</code> would also be the same since it's just the root route <code>/</code>.</p><p>So, by logical deduction, <code>$proxy_host</code> had to have changed to somehow be the same and cause root requests to different services to overlap.</p><p>Well, according to <a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#var_proxy_host" title="this">this</a>, <code>$proxy_host</code> is defined as the "name and port of a proxied server as specified in the&nbsp;<a href="http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_pass">proxy_pass</a>&nbsp;directive".</p><p>And how is <code>proxy_pass</code> defined? Well, the only way to find that would be to check the Ingress' generated <code>nginx.conf</code>. A quick <code>kubectl exec -it &lt;nginx pod&gt; -- cat /etc/nginx/nginx.conf</code> later and we look what we've got here! I'm not going to post the whole config (cause it's huge), but guess what? Every instance of <code>proxy_pass</code> is the same! </p><p>They're all defined as:</p><pre><code>proxy_pass http://upstream_balancer;</code></pre><p>Well, what the heck is <code>upstream_balancer</code>? Searching through the rest of the config file, we find this:</p><pre><code>upstream upstream_balancer {
    ### Attention!!!
    #
    # We no longer create "upstream" section for every backend.
    # Backends are handled dynamically using Lua. If you would like to debug
    # and see what backends ingress-nginx has in its memory you can
    # install our kubectl plugin https://kubernetes.github.io/ingress-nginx/kubectl-plugin.
    # Once you have the plugin you can use "kubectl ingress-nginx backends" command to
    # inspect current backends.
    #
    ###
    
    server 0.0.0.1; # placeholder
    
    balancer_by_lua_block {
        balancer.balance()
    }
    
    keepalive 320;
    
    keepalive_timeout  60s;
    keepalive_requests 10000;  
}</code></pre><p>Because all of the <code>proxy_pass</code> values are the same, the <code>$proxy_host</code> value is the same for all requests. Which makes our cache keys overlap at only the route level, rather than the host + route level.</p><p>Looks like we found our culprit!</p><p>But now, how do we fix it? Presumably, we need to replace <code>$proxy_host</code> with something else, since the <code>$scheme</code> and <code>$request_uri</code> should be fine.</p><p>Thankfully, the <code>proxy_cache_key</code> directive lists the following as an example:</p><pre><code>proxy_cache_key "$host$request_uri $cookie_user";</code></pre><p>This would be for adding cookies to the cache key. But what's more important is the use of the <code>$host</code> variable. According to <a href="http://nginx.org/en/docs/http/ngx_http_core_module.html#var_host" title="the docs">the docs</a>, it should take the hostname from the request itself. Which should suit our needs just fine!</p><p>And yep, changing the <code>proxy_cache_key</code> to:</p><pre><code>proxy_cache_key "$scheme$host$request_uri";</code></pre><p>Seems to fix the issues. No more infinite request loops, no more wrong home pages. Success!</p><p>And that's how my 6-hour <code>cert-manager</code> upgrade turned into a 6-hour <code>ingress-nginx</code> upgrade.</p><div><hr></div><p>What, did you think this was the last of the DevOps detour? Oh no, we've still got a lot more to go. From Docker image caching problems to improving our monitoring suite, there's probably another 2 or 3 parts to go.</p><p>Now whether or not I get the <em>ambition</em> to write them all is a different matter... So, stay tuned for potentially more rounds of the DevOps Detour!</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #7]]></title><description><![CDATA[Reflections on Launching, Two Weeks Later]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-7</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-7</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Wed, 05 May 2021 16:26:24 GMT</pubDate><enclosure url="https://cdn.substack.com/image/fetch/h_600,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today, on matters concerning my existence, it's still <a href="https://ufincs.com" title="uFincs">uFincs</a>. Why would it ever not be uFincs? Well, you might have been expecting Part 3 of the DevOps Detour (check out <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-6" title="Part 2">Part 2</a>), but today we're gonna be tackling the more business-y side of things, rather than the technical side.</p><p>In particular, have you heard that <a href="https://blog.ufincs.com/p/announcing-ufincs" title="uFincs has officially launched">uFincs has officially launched</a>? Yeah, let's talk about that.</p><h1>Last Time</h1><p>If you remember all the way back in <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-4" title="uFincs Update #4">uFincs Update #4</a> at the end of March, I was talking about all my wonderful launch plans and how I was gonna take the month of April to really focus on marketing and launching.</p><p>Well, it's now May, so it seems like now's the best time to reflect on how that went.</p><h1>Procrastination</h1><p>As much as I had wanted to set aside the whole month for marketing/launch efforts, that's not exactly how it played out. As evidenced by <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-5" title="uFincs Update #5">uFincs Update #5</a> and <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-6" title="uFincs Update #6">uFincs Update #6</a>, I took a couple weeks detour to deal with some DevOps-related tasks that I had been putting off (upgrading the GKE cluster, setting up monitoring, that kind of thing). </p><p>This ended up claiming the first week of April.</p><h1>Setting up an Official Blog</h1><p>The second week of April I had a realization that I should really set up an official blog for uFincs. Partly so that I could have one place that would have the canonical 'launch' post that I could then link to elsewhere, and partly to have a medium for capturing emails/sign-ups for anyone that didn't want to immediately sign up for uFincs itself. </p><p>And so, <a href="https://blog.ufincs.com" title="blog.ufincs.com">blog.ufincs.com</a> was born.</p><p>I decided to just host it on Substack because, well, it's damn simple. Plus it's where I host my personal blog (i.e. what you're currently reading) so it made sense from a consolidation POV as well.</p><p>Although, I did run into a snag when trying to set up the custom domain for the blog. Bought the $50 custom domain add-on from Substack, set up the specified DNS records, and... nothing.</p><p>All I got was a generic error when trying to finish the setup process:</p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tOSL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tOSL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 424w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 848w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 1272w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tOSL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png" width="1456" height="770" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:770,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:136926,&quot;alt&quot;:&quot;Substack Error&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Substack Error" title="Substack Error" srcset="https://substackcdn.com/image/fetch/$s_!tOSL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 424w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 848w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 1272w, https://substackcdn.com/image/fetch/$s_!tOSL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F444f4250-4c9a-46df-b0d9-0a0f872ce92d_1852x980.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As you can imagine, not particularly helpful.</p><p>Thankfully, Substack's support <em>was</em> helpful. After one support reply that effectively amounted to "Have you tried turning it off and on again?" (yes, thank you), it was quickly escalated to an engineer who "made a fix on the Cloudflare end". Couple hours' turnaround; pretty good Substack!</p><p>After that, it was just a matter of stretching my marketing copy muscles to pump out the <a href="https://blog.ufincs.com/p/announcing-ufincs" title="announcement post">announcement post</a>. </p><p>In retrospect, the official blog didn't end up making much of an impact. Didn't get any new newsletter subscribers (which, considering it's a blog specifically for content marketing, I'm not surprised), and the announcement post got less than 50 views. Although, I believe that has more to do with me not using the announcement post as effectively as I could, in addition to the Blog link on the <a href="https://ufincs.com">marketing site</a> not being particularly well-positioned as a CTA. Will have to improve that in the future.</p><p>Anyways, at least I can feel good about having an 'official' blog (as opposed to <em>this</em> blog, which is utterly unofficial, even though it's the more interesting one :).</p><p>Besides setting up the official blog, that second week of April was spent writing the posts for my various launch targets: LinkedIn, Startup School, and Indie Hackers. Let's go through each of those launches, one by one.</p><h1>LinkedIn Launch</h1><p>LinkedIn was the first place I launched uFincs. This made the most sense to me for mostly one reason: so that I could finally fill the hole in my 'employment' history. Yes, it has bugged me that, because I didn't have uFincs officially on my resume, there was a 1+ year gap since I graduated into a global pandemic. Superficial, I know.</p><p>This also gave me the wonderful opportunity to set up an <a href="https://www.linkedin.com/company/ufincs" title="official company page">official company page</a>!</p><p>As you might guess, I do like my 'official' things. </p><p>Anyways, on Monday, April 19th, I did three things: put up a <a href="https://www.linkedin.com/posts/ufincs_announcing-ufincs-activity-6790077398856601600-zigK" title="post">post</a> on the company page, put up a <a href="https://www.linkedin.com/posts/devin-sit_announcing-ufincs-activity-6790077824100315136-7GLy" title="post">post</a> from my personal account, and finally updated my employment history. Besides having a fairly small network (only ~50 people), I wasn't really expecting much considering it's, ya know, <em>LinkedIn</em>. Who's actively checking LinkedIn, of all places, for exciting news?! </p><p>Exactly.</p><p>Although, I was banking on one thing: the fact that this was my first post. I've seen LinkedIn app notifications come in explicitly for people who made their first post, so I was thinking that I might be able to take advantage of that system for the uFincs launch. Did it work? No idea; didn't ask anyone. </p><p>I was also expecting a notification to go out for my employment update, but again, no idea on that front.</p><p>In the end, thanks to a core few people liking and commenting on the post, I was able to get just over 600 'views' on it. Pretty good for a network of only 50!</p><p>Not only that, but I got first my (paying!) customer as a direct result of the personal post. They ended up being a 2nd level connection, before upgrading to 1st level when they inquired about uFincs (and subsequently bought a subscription).</p><p>Hey, considering my expectations were basically nil, I'd say getting my first customer through LinkedIn is pretty good!</p><p>As far as the official company post went, it got basically zero traction. Again, not surprising. Probably should have picked some more niche hashtags to slap on it, but I'm a noob to social media in general, so yeah...</p><p>Anyways, I'd call the LinkedIn launch a success!</p><p>The Startup School launch, though, is a different story...</p><h1>Startup School Launch</h1><p>So, why did I decide on Startup School (SUS) as my second launch point? Well, I'd been through the SUS curriculum, became "SUS Certified", and generally knew it as a positive place to launch, so it just made sense to me. Of course, I also knew that the audience wasn't particularly large, so I wasn't expecting much in the way of a response.</p><p>On Wednesday, April 21st (two days after the LinkedIn launch), I made <a href="https://www.startupschool.org/posts/41903" title="this post">this post</a> to the "Show SUS" section of the Startup School forums.</p><p>And... crickets.</p><p>No upvotes. No comments. Nada.</p><p>About what I expected. It seems to be very 50/50 in terms of Show SUS posts over there.</p><p>However, it wasn't until several days later (on Monday, April 26th &#8212; coincidentally, the same day that I got my first customer from LinkedIn) that I received <a href="https://www.startupschool.org/posts/41903#comment-185217" title="this comment">this comment</a>:</p><blockquote><p>This - is - f***ing - amazing. </p><p>I have no idea if the idea is any good because it's not really my area at all (I guess you'll find out!) but the execution on this is incredible. </p><p>Come join our company if this doesn't take off. The pay is sh*t and the hours are worse - but real recognizes real my friend. </p><p>Very nicely done.</p></blockquote><p>Needless to say, I was having a very good day that day. It was definitely nice to finally have some external validation for all the elbow grease I'd put into uFincs. Especially validation that was so... strongly worded :) </p><p>Other than that, I got less than 10 pageviews from SUS, so I wouldn't exactly call this launch a success. But it was good to get out of the way.</p><p>Up next, Indie Hackers.</p><h1>Indie Hackers</h1><p>Indie Hackers is an... interesting forum. If I had to summarize it as &#8212; effectively &#8212; an outsider, it'd be "the place to self-promote to others trying to self-promote". </p><p>Cause really, most everyone posting there has a higher goal than just "posting for the fun of it". Obviously, myself included!</p><p>So, time to get in on this 'self-promotion' action. First things first, got to set up another <a href="https://www.indiehackers.com/product/ufincs" title="'official' company page">'official' product page</a>! Surely no one will take you seriously without a product page.</p><p>Well, as it would turn out, no one was gonna take me seriously, period. At least, as long as we define "take seriously" as "visit my web page in large quantities".</p><p>After creating the product page, I figured I had to do some research about which group to put my launch post in. And there's a <em>lot</em> of <a href="https://www.indiehackers.com/groups" title="groups">groups</a> on Indie Hackers.</p><p>Having come from Startup School (and more generally, Hacker News), I looked (and found) a <a href="https://www.indiehackers.com/group/showih" title="&quot;Show IH&quot; group">"Show IH" group</a>, so I figured that was the place to launch things on Indie Hackers... Until I realized that it was listed under the "Inactive Groups" section. And perusing the posts in the group definitely reinforced that feeling.</p><p>Yeah, that didn't seem like a good idea.</p><p>It was at this point that I figured that Indie Hackers was less about directly launching products and more about writing vaguely relevant posts that somehow feed back to your product. I mean, meta time, <em>this post</em> is no different. So, figuring that the actual launch post wasn't gonna matter much either way, I decided that the <a href="https://www.indiehackers.com/group/landing-page-feedback" title="Landing Page Feedback">Landing Page Feedback</a> group is where I'd launch.</p><p>Side note: It's only as I'm literally writing this that I realized there <em>is</em> a <a href="https://www.indiehackers.com/group/product-launch" title="Product Launch">Product Launch</a> group. So... :facepalm: on me for that. In my defence, it was more towards the bottom of the "Fastest Growing" section and, as I said, there's a <em>lot</em> of groups. Goes to show just how much of an outsider I am.</p><p>Anyways, on Friday, April 23rd (two days since the SUS launch, four since the LinkedIn launch) I threw up <a href="https://www.indiehackers.com/post/recently-launched-ufincs-what-do-you-think-81ea801296" title="this sad excuse for a post">this sad excuse for a post</a> in the Landing Page Feedback group. </p><p>And just like SUS, crickets. To this day, I still haven't gotten any response on it and only about 5 page views for <a href="https://ufincs.com" title="ufincs.com">ufincs.com</a> came from Indie Hackers. Figured as much.</p><p>Like I always say, as long you set your expectations to zero, you can never be disappointed!</p><h1>Launch Thoughts (So Far)</h1><p>And speaking of zero expectations, that wraps up the first three launches for uFincs. I went in expecting nothing and came out with a paying customer and a strongly worded compliment. </p><p>I'll take that!</p><p>However, this isn't actually the end of the story. I would be hit with a surprise one morning only a couple of days later...</p><h1>Hacker News Soft-Launch</h1><p>On Wednesday, April 28th, two days after acquiring my first customer, I woke up in the morning and found <a href="https://news.ycombinator.com/item?id=26969173">this post</a> near the top of Hacker News:</p><blockquote><p>Show HN: I made a simulator for personal finance</p></blockquote><p>Oh boy. Anytime something about personal finances shows up on Hacker News, it usually does pretty well. This particular post would end up getting nearly 500 upvotes, which is <em>really</em> good for a "Show HN". </p><p>I figured that, even though I hadn't officially launched uFincs on Hacker News, it would still be a good idea (from a marketing POV) to self-promote uFincs in this kind of thread. It was already on topic, so I just needed the catalyst... </p><p>And yep, there it is. What was, at the time, the <a href="https://news.ycombinator.com/item?id=26969526">top comment</a> in the thread:</p><blockquote><p>Looks interesting, but based on the privacy policy terms of data usage, transferability, and so forth, I can't in good faith test with real financial data...</p></blockquote><p>So now, not only did I have a relevant thread (personal finances), but I had a relevant <em>top comment</em> about data privacy. This was the perfect storm of opportunity. I couldn't pass this up. </p><p>And you bet I <a href="https://news.ycombinator.com/item?id=26970716" title="took it">took it</a>.</p><p>For context, I have a private Slack channel hooked up to the Backend of uFincs so that I can receive notifications for when various things occur. Namely, when people sign up, subscribe to a plan, or enter what I'll refer to as <a href="https://ufincs.com/noaccount" title="'no-account' mode">'no-account' mode</a>. </p><p>Within 10 seconds of posting my comment, I was already getting Slack notifications.</p><p>Within a minute, the channel had already filled the screen with messages from users entering 'no-account' mode.</p><p>Within a couple minutes, I had to mute the channel to save myself from going insane.</p><p>In fact, you know how Slack inserts timestamps every now and then to group together blocks of messages?</p><p>It took more than <em>4 hours</em> before things slowed down enough for Slack to put a new timestamp:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!wsaV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!wsaV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 424w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 848w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 1272w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!wsaV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png" width="881" height="852" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/a21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:852,&quot;width&quot;:881,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:89821,&quot;alt&quot;:&quot;A giant wall of slack messages&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A giant wall of slack messages" title="A giant wall of slack messages" srcset="https://substackcdn.com/image/fetch/$s_!wsaV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 424w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 848w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 1272w, https://substackcdn.com/image/fetch/$s_!wsaV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa21d7fbd-af1d-4909-a673-5ba83a4844d8_881x852.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>...</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PT-7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PT-7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 424w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 848w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 1272w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PT-7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png" width="878" height="461" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/d1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:461,&quot;width&quot;:878,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:51464,&quot;alt&quot;:&quot;The end of the giant wall of slack messages before a new timestamp is added&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="The end of the giant wall of slack messages before a new timestamp is added" title="The end of the giant wall of slack messages before a new timestamp is added" srcset="https://substackcdn.com/image/fetch/$s_!PT-7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 424w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 848w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 1272w, https://substackcdn.com/image/fetch/$s_!PT-7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fd1771e67-4619-4546-849b-ad2e79bf1a91_878x461.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>My whole day was just sitting at my computer, watching the charts on my monitoring dashboard, watching the web traffic from Simple Analytics, and responding to comments/questions. </p><p>It. Was. Wild.</p><p>Received basically nothing but positive feedback. Tons of people checked out the app and marketing site. Even had a couple people reach out personally to say that they had been working on similar ideas and wanted to see about potential collaboration opportunities!</p><p>Honestly, I'm just glad nothing fell apart. It's good to know that my infrastructure could handle this kind of load without breaking a sweat. I mean, absolutely speaking, it wasn't <em>that</em> many views; only a couple thousand page views and around 1k unique visitors. But it was <em>wayyy</em> more than anything the system had seen before. </p><p>For context, here's how Nginx (our internal load balancer) fared. First, CPU usage (where 1000ms/s = 100% load, I believe):</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3u5W!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3u5W!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 424w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 848w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 1272w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3u5W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png" width="507" height="316" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/03283594-c362-43d5-a368-d39442a3ca1e_507x316.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:316,&quot;width&quot;:507,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:24293,&quot;alt&quot;:&quot;CPU usage of Nginx for the whole day, showing a spike when I first posted my comment&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="CPU usage of Nginx for the whole day, showing a spike when I first posted my comment" title="CPU usage of Nginx for the whole day, showing a spike when I first posted my comment" srcset="https://substackcdn.com/image/fetch/$s_!3u5W!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 424w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 848w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 1272w, https://substackcdn.com/image/fetch/$s_!3u5W!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F03283594-c362-43d5-a368-d39442a3ca1e_507x316.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And now requests/second:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3BXZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3BXZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 424w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 848w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 1272w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3BXZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png" width="527" height="305" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:305,&quot;width&quot;:527,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:18298,&quot;alt&quot;:&quot;Nginx requests rate for the whole day, showing a spike when I posted my comment&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Nginx requests rate for the whole day, showing a spike when I posted my comment" title="Nginx requests rate for the whole day, showing a spike when I posted my comment" srcset="https://substackcdn.com/image/fetch/$s_!3BXZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 424w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 848w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 1272w, https://substackcdn.com/image/fetch/$s_!3BXZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F42d78536-874c-4a7f-a10e-2bb7d9cbf1a8_527x305.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Obviously, that spike around 12:00 PM is right around when I posted my first comment.</p><p>In fact, during all this, not only did uFincs stay up and running, but <em>Hacker News itself</em> went down!</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KdfL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KdfL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 424w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 848w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 1272w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KdfL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png" width="400" height="94" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:94,&quot;width&quot;:400,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:6698,&quot;alt&quot;:&quot;Hacker News showing an error message that the site isn't working &quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Hacker News showing an error message that the site isn't working " title="Hacker News showing an error message that the site isn't working " srcset="https://substackcdn.com/image/fetch/$s_!KdfL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 424w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 848w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 1272w, https://substackcdn.com/image/fetch/$s_!KdfL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F3b52ed18-48fd-4859-a75c-bd2ddaab8253_400x94.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!8VAO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!8VAO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 424w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 848w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 1272w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!8VAO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png" width="347" height="108" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/a9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:108,&quot;width&quot;:347,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:9360,&quot;alt&quot;:&quot;Hacker News' status page showing that there was an outage during the time when my comment was generating the most traffic&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Hacker News' status page showing that there was an outage during the time when my comment was generating the most traffic" title="Hacker News' status page showing that there was an outage during the time when my comment was generating the most traffic" srcset="https://substackcdn.com/image/fetch/$s_!8VAO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 424w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 848w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 1272w, https://substackcdn.com/image/fetch/$s_!8VAO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2Fa9d1a486-2bf5-44a5-a8e0-fd0a42fffbb9_347x108.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Man, didn't think I'd be reverse-hug-of-death'ing Hacker News that day! /s</p><p>On top of all that, I even pushed a production deploy to fix a little something in the Backend, while everything was going on. Yes, I'm <em>that</em> crazy.</p><p>Finally, here's the marketing site analytics (again, courtesy of Simple Analytics) to really bring it home:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!h4iW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!h4iW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 424w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 848w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 1272w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!h4iW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png" width="801" height="857" data-attrs="{&quot;src&quot;:&quot;https://bucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com/public/images/72d23146-1c1d-4462-892c-955921537ad7_801x857.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:857,&quot;width&quot;:801,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82822,&quot;alt&quot;:&quot;A graph showing page views over time, with a spike after I posted my first comment&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="A graph showing page views over time, with a spike after I posted my first comment" title="A graph showing page views over time, with a spike after I posted my first comment" srcset="https://substackcdn.com/image/fetch/$s_!h4iW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 424w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 848w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 1272w, https://substackcdn.com/image/fetch/$s_!h4iW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fbucketeer-e05bbc84-baa3-437e-9518-adb32be77984.s3.amazonaws.com%2Fpublic%2Fimages%2F72d23146-1c1d-4462-892c-955921537ad7_801x857.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That first noticeable drop at 1:00 PM is, as far as I remember, when the top comment I replied to no longer became the top comment. And then everything just kept falling as the thread itself fell down the front page of Hacker News.</p><p>But still, I'd consider this a wild success. In my opinion, this soft-launch completely validates my thoughts that uFincs is <em>indeed</em> solving a hole in the market. Everyone who's given me feedback has only had positive things to say, whether it be the execution/design of the app itself or the idea as a whole. Of course, there were those that complained about the pricing, but that's to be expected (I'd be more surprised if they <em>didn't</em> complain).</p><p>And it's all that much sweeter once you take into account that I got my second paying customer late into the night that day. The cherry on top, as they say.</p><p>Obviously, my traffic levels have since fallen far off that peak. But I'm still averaging ~50 page views a day on the marketing site and maybe a dozen or so 'no-account' logins. Which is a <em>significant</em> improvement over the big fat ZERO from before. This also means that word-of-mouth is now at play &#8212; the most elusive of marketing strategies.</p><p>All this, from only a one-off comment! Albeit, a one-off comment in a thread where the involved parties were likely to be solid leads. But still, I wonder just how much crazier a Show HN of my own will be. Honestly, at this point, I can only see two outcomes:</p><ol><li><p>It sees absolutely zero traction whatsoever (because I didn't pray to the right Hacker News gods that day).</p></li><li><p>It utterly explodes. </p></li></ol><p>Considering the traction of the post that I hijacked, I think it's fair to say that 2. is a likely outcome. I wouldn't expect my post to be <em>only</em> 'mildly well received', but who knows &#8212; it's the internet after all.</p><p>Of course, there's only one way to find out. I'm sure my own Show HN post will be happening sometime in the near future, so keep an eye out for it!</p><h1>Final Thoughts</h1><p>So yeah, of my original launch targets (LinkedIn, Startup School, and Indie Hackers) only LinkedIn really panned out. And I knew that uFincs would do well on Hacker News, I just hadn't planned to get there so soon. </p><p>So what's next? Well, I think a Show HN post of my own will be my next launch target. Like I said above, that should probably happen in the next week or two. Hopefully that drives enough traffic/word-of-mouth to get to my elusive "10 paying customers" goal. It's at that point where everything basically starts turning to gravy.</p><p>Of course, I need to keep up the blogging/content marketing. SEO is, based on my research, a long-term game, so I just gotta keep pumping out these posts in the hope that it pays off in the future (well, more so for the official uFincs Blog than my own personal blog; although I like to think that my personal blog can make for some good forum posts :).</p><p>Other than Hacker News, I still have various subreddits and Product Hunt on my radar. I'll probably be doing a couple subreddit launches over the course of May, but Product Hunt will definitely be deferred for a later month. I want to have a decent customer base before I launch there, to show some level of 'maturity' (and by 'decent', I'm really just thinking &gt; 10).</p><p>But yeah, very exciting times. I've finally gone from burning money with no income whatsoever to burning money while making a small bit of income! Surely, one of the hardest steps for any entrepreneur to take. It's only up from here!</p><p>Till next time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #6]]></title><description><![CDATA[DevOps Detour - Part 2]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-6</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-6</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Sun, 25 Apr 2021 23:00:53 GMT</pubDate><content:encoded><![CDATA[<p>OK, time for round 2 of the <a href="https://ufincs.com" title="uFincs">uFincs</a> DevOps Detour that oh so concerned my existence several weeks ago.</p><h1>Last Time</h1><p><a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-5" title="Last time">Last time</a> was all about backups. Encrypting backups, copying backups, deleting backups, but, most importantly of all, restoring backups.</p><p>Now it's cluster time.</p><h1>Cluster Time</h1><p>There were a handful of different cluster-related tasks that I wanted to take care of, but the more I did, the more tasks I found to do. Particularly security-related ones. Once I found the <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster" title="GKE security hardening guide">GKE security hardening guide</a>, it was just a rabbit hole of tweaking and configuring, all in the hopes of 'making things more secure' (in theory).</p><p> So... I'll just go through them one by one.</p><h2>Upgrading Kubernetes Version</h2><p>For the longest time, I always specified the Kubernetes version directly in Terraform.</p><p>Well, turns out there's a better way.</p><p>Introducing: Google Kubernetes Engine (GKE) <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/release-channels" title="release channels">release channels</a>! Just opt into a release channel (Rapid, Regular, or Stable) and GCP will handle upgrading the cluster for you!</p><p>Yeah, I'm pretty sure this feature didn't exist when first wrote the Terraform configs, otherwise I would have just opted into a release channel to start with.</p><p>Better late than never!</p><p>I decided to go with the Stable channel. I don't really make use of bleeding-edge Kubernetes features, and I'd rather things <em>didn't</em> break with my production cluster, so it only made sense.  That left us on the latest patch of v1.17, one whole minor version up from our previous v1.16.</p><p>But at least now I don't really need to worry about version upgrades. Just let GCP upgrade the masters and nodes and that's good enough for me.</p><h2>Enabling Shielded VMs</h2><p><a href="https://cloud.google.com/shielded-vm" title="Shielded VMs">Shielded VMs</a> are, in Google's words, "virtual machines (VMs) on Google Cloud hardened by a set of security controls that help defend against rootkits and bootkits".</p><p>In <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/shielded-gke-nodes" title="GKE terms">GKE terms</a>, shielded nodes "limit the ability of an attacker to impersonate a node in your cluster".</p><p>This is nice and all but what really matters to me is that:</p><ol><li><p>They are 'more secure' (for some definition of secure).</p></li><li><p>They are easily turned on.</p></li><li><p>They don't cost anything extra.</p></li></ol><p>Yep, just <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#enable_shielded_nodes" title="flip a switch">flip a switch</a> in the Terraform config for the cluster and everything becomes more magically secure, for free!</p><p>Hard to argue with that, so I figured it'd be a good thing to turn on.</p><h2>Enabling Secure Boot</h2><p>Good old UEFI Secure Boot. The bane of every developer wanting to install Ubuntu on their new Windows laptop (or maybe that's just me).</p><p>Anyways, enabling <a href="https://cloud.google.com/security/shielded-cloud/shielded-vm#secure-boot" title="Secure Boot">Secure Boot</a> for GKE nodes "helps ensure that the system only runs authentic software by verifying the digital signature of all boot components".</p><p>But again, it's just a <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#enable_secure_boot" title="flag">flag</a> to turn on, and it's free, so on it goes in the name of 'security'!</p><h2>Switching from Docker to containerd</h2><p>This is a slightly more interesting decision than just "free + more secure = turn on".</p><p>See, <a href="https://kubernetes.io/blog/2020/12/02/dont-panic-kubernetes-and-docker/" title="Kubernetes announced">Kubernetes announced</a> that they were deprecating Docker as a runtime, in favour of containerd (basically, just a different but compatible container runtime).</p><p>Now, I'm not actually sure if there are any security benefits to making this change, but since it's a change we'd have to make <em>eventually</em> (and it's just a <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/container_cluster#image_type" title="config option">config option</a> to change), I figured we might as well get ahead of the curve, validate that our workloads work with the change, and just commit to it.</p><p>Saves future me some trouble.</p><h2>Switching from NodePort to ClusterIP Services</h2><p>In order to understand this change, you first need to understand a bit of the internal architecture of our cluster.</p><p>See, instead of using GKE's built-in ingress, we actually use <a href="https://kubernetes.github.io/ingress-nginx/" title="ingress-nginx">ingress-nginx</a> (not to be confused with the <a href="https://www.nginx.com/products/nginx-ingress-controller/" title="Nginx Ingess">Nginx Ingress</a>). This is so that we only have to provision a single load balancer for the entire cluster, rather than having to provision one for each individual service (which would get <em>extremely</em> expensive very fast).</p><p>In effect, <code>ingress-nginx</code> acts as an internal load balancer for the cluster. Network requests first hit the GCP load balancer, which then get routed to <code>ingress-nginx</code>, which then routes the request to the correct service based on the request's subdomain.</p><p>This is especially effective for our use case since <code>ingress-nginx</code> can even route <em>between namespaces</em>. This means it can handle our production namespace as well as all our per-branch namespaces. </p><p>At least when I first came up with this cluster architecture, the GKE ingress couldn't do that (and I haven't researched it since, so I still don't know). But considering how expensive load balancers are on GCP, only having to have one is a <em>boon</em> in terms of keeping costs low.</p><p>Anyways, this is all to say that, when I first set up the cluster, I had configured all of the main services to be of type <code>NodePort</code>. Why? Cause... it worked.</p><p>But then recently, after looking over some other random cluster architecture diagrams, I had the realization that "wait a minute, if <code>ingress-nginx</code> performs all of the routing in-cluster, then couldn't the services just be <code>ClusterIP</code>?".</p><p>As it turns out, yes, yes they can.</p><p>So now the services don't even need to expose a port on every node, once again reducing the attack surface &#8212; if only by a little bit.</p><h2>Switching to Two Nodes (from One)</h2><p>Yes, for the longest time I only ran the cluster from a single node &#8212; first an <a href="https://cloud.google.com/compute/docs/machine-types#n1_machine_types" title="n1-standard-1">n1-standard-1</a> node before 'upgrading' to an <a href="https://cloud.google.com/compute/docs/machine-types#e2_shared-core_machine_types" title="e2-medium">e2-medium</a>. I also use preemptible nodes, so the one node that ran everything would disappear <em>at least</em> every day (before being replaced by another node). All in the name of cost savings.</p><p>Needless to say, I finally got annoyed by the daily uptime alerts that the site was down (although even that was good enough for something like 99.5% uptime).</p><p>So I finally relented and decided to add a mere <em>second</em> node to the cluster. Although, by itself, this wasn't really enough to prevent the site from going down; it merely reduced the chances that the node that was running everything would get preempted.</p><p>In order to get 'true' high availability, we'd need to make some other improvements...</p><h2>Anti-Affinity Deployments</h2><p>And here comes a really helpful feature of Kubernetes deployments: <a href="https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity" title="pod anti-affinity">pod anti-affinity</a>.</p><p>Basically, 'pod affinity' is a Kubernetes feature that enables pods to be scheduled onto nodes given a certain set of rules. For example, if a pod needs access to a node with a GPU attached, then an affinity rule could specify as much.</p><p>But for our purposes, <em>anti-affinity</em> is more useful. These kinds of rules allow us to basically say "don't schedule pods of the same deployment on the same node". In effect, the replicas of a deployment will get evenly spread across all our nodes, as long as we have enough nodes.</p><p>So, to make sure uFincs is 'highly available' (or HA), all we need to is up the number of replicas for each service to 2 (to match the new number of nodes), add an anti-affinity rule, and voila!</p><p>Or so I thought.</p><p>See, these anti-affinity rules currently only happen during <em>scheduling</em>, not after the replicas are already running. It's important to understand this distinction to understand why this setup doesn't actually get us to HA yet.</p><p>Let's take the example of a fresh deployment. We have 2 nodes and each service has 2 replicas, one on each node. </p><p>But what happens when a node gets preempted? Well, the node will disappear from the cluster and the replicas on it will be (forcefully) terminated. Kubernetes will then see that the replica count no longer matches the desired state and will schedule the second replicas onto the remaining node. After some undetermined amount of time, GCP will provision a new (second) node to fill in for the node that was just preempted.</p><p>The key thing here is that, although not strictly deterministic AFAIK, the new node gets added to the cluster <em>after</em> Kubernetes has already re-scheduled the replicas. And since pod anti-affinity rules only (currently) work during scheduling (as stated above), those replicas just stay there. On that one node. Forever.</p><p>And then I get an uptime alert when that one node inevitably gets preempted.</p><p>So how do we fix this? How do we get Kubernetes to 'rebalance' the replicas after scheduling?</p><p>Well, as far as I'm aware, Kubernetes doesn't have a built-in way to handle this. Supposedly, there are plans to change this so that the affinity rules can work <em>after</em> scheduling, but this isn't a thing yet.</p><p>So we hack around the problem. Introducing: the Descheduler.</p><h2>Adding the Descheduler</h2><p>The <a href="https://github.com/kubernetes-sigs/descheduler" title="Descheduler">Descheduler</a> is ultimately a ridiculously simple solution to our 'rebalancing' problem: just run a cronjob every so often that kills duplicate pods on every node.</p><p>Yes, that's right, just kill them.</p><p>Then Kubernetes will <em>have</em> to reschedule the replicas and, assuming we have both nodes available, the new replicas will get put onto the second node.</p><p>Tada! Problem solved. Clean, simple, efficient.</p><p>Really, the only decision there was to be made here was <em>how often</em> to run the cronjob. Since nodes are preempted <em>at least</em> once every 24 hours, I figured running the job hourly should be enough to keep the cluster balanced. That is, I wouldn't expect both nodes to be preempted within one hour of each other. Is it possible? Of course, but it's not like the costs of assuming this incorrectly are particularly high.</p><p>Anyways, the cronjob seems rather lightweight to run, so increasing its run frequency isn't exactly a terrible thing to do.</p><p>Point is, with two nodes, pod anti-affinity rules, and this magical descheduler, we now have a <em>minimal</em> setup for high availability.</p><p>Could we go further? Of course. More nodes, more replicas, etc. But seeing as how uFincs is just starting out (and has little load on it), there's no reason to do so yet. I mean, you could (rightfully) argue that there's no reason to even be using Kubernetes <em>at all</em> for a service like uFincs.</p><p>But if we <em>did</em> want to scale to the moon, the engineer in me enjoys the fact that it wouldn't be much effort.</p><div><hr></div><p>Well, I think that's been more than enough for one post. Stay tuned for Part 3 of the DevOps Detour!</p><p>How many parts will there be, you ask? No idea.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #5]]></title><description><![CDATA[DevOps Detour - Part 1]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-5</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-5</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Sun, 18 Apr 2021 02:13:57 GMT</pubDate><content:encoded><![CDATA[<p>Today, in the inconsistently scheduled <a href="https://ufincs.com">uFincs</a> Update series, we're taking a little detour. </p><p>DevOps is the matter concerning me today. And as much as the 'dev' and 'ops' teams at uFincs are <em>highly</em> integrated, it's more about Devin + Ops than anything :)</p><h1>Last Time</h1><p><a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-4">Last time</a> was all about being positive, talking about business prospects and marketing strategies. You know, the things I <em>should</em> be focusing on.</p><p>Well, this isn't a 'detour' for nothing.</p><h1>Deciding What to Do</h1><p>As last month came to a close, I was put into an awkward position in terms of my scheduling.</p><p>See, on the first of each month, I usually put together a plan for what I want to accomplish that month and how I'm going to do it. I also do similar planning sessions at the start of each week.</p><p>Well, April 1st awkwardly landed towards the end of the week. So, instead of planning April early (which, as I laid out in <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-4" title="Update #4">Update #4</a>, was gonna be all about marketing), I decided to deal with some DevOps tasks that I had been putting off.</p><p>I figured they'd be a good break from the usual frontend/backend work of feature development and be relatively quick to do.</p><p>HA! </p><p>tl;dr one thing just led to another and this detour took a bit longer than I expected.</p><h1>Improving Backups</h1><p>For the longest time, uFincs' database backup solution was a daily Kubernetes cronjob that dumped the whole database and saved it to a Google Cloud Storage (GCS) bucket. There were a couple of improvements I had been meaning to make to this system:</p><ol><li><p>Encrypt the backups.</p><ul><li><p>Why? Cause other people say to encrypt your backups. </p></li><li><p>I mean, I guess this prevents against an attacker from breaking into my GCP account and stealing the plaintext backups, but there are worse things to worry about in case of an account breach.</p></li><li><p>I suppose this is more relevant to organizations where employee count &gt; 1, to prevent unauthorized data access, but nonetheless, I figured it'd be a good layer of protection to have.</p></li></ul></li><li><p>Auto delete old backups.</p><ul><li><p>I mean yeah, it's good to have, especially considering I've got some 1+ year-old backups at this point.</p></li><li><p>Not to mention I've got a bunch of generally pointless backups lingering around, thanks to our deployment setup (per-branch deployments courtesy of <a href="https://github.com/DevinSit/kubails" title="Kubails">Kubails</a>).</p></li></ul></li><li><p>Copy backups somewhere else.</p><ul><li><p>Remember kids, 3-2-1 backups, so having a separate + remote place to keep a copy of backups is always good.</p></li><li><p>I figured an S3 bucket in AWS is 'good enough' for our purposes.</p></li></ul></li><li><p>Restore the latest backup to new namespaces.</p><ul><li><p>Like I said above, we make use of per-branch deployments. This means that every feature branch in development gets its own namespace on our Kubernetes cluster so that we can fully deploy and test every change we make before it hits production. But because production runs using the same mechanism (in the same cluster!), that means that each 'feature namespace' is, in itself, effectively a copy of production.</p></li><li><p>However, it <em>isn't</em> a copy of production since the database only gets bootstrapped with the basic seed data for a test account. Or at least, it did until we decided to make this change.</p></li><li><p>By restoring the latest backup to each feature branch namespace, we'd accomplish 2 things:</p><ol><li><p>We'd be able to test, using production data, everything new that we develop. This also means that getting other users to test new features would be very easy since their data would be available.</p></li><li><p>It'd satisfy the 'backup system requirement' to always be testing your backups (you know, "an untested backup isn't a backup"). And having backups tested on the frequent basis that is our development work? Even better!</p></li></ol></li><li><p>And since all user data is encrypted, even if something did go wrong during development, the chances of something going is greatly reduced.</p></li></ul></li></ol><p>So yeah, these 4 things would make the uFincs backup system damn near bulletproof. </p><p>The only weakness is that, as I must sadly admit, I haven't yet delved into dealing with WAL backups (FYI, we use Postgres) for point-in-time recovery. As of yet, I've been content with just daily backups, considering the meagre size of our database. </p><p>And that will probably suffice for quite a while. If things start to pick up, I'll probably switch to twice-daily or even more frequent backups, but for now, this works.</p><p>Anyways, getting these 4 improvements in place only took about 2 days.</p><h2>Encrypting Backups</h2><p>Encrypting backups was done with a simple GPG key and AES-256. Encrypt the backup files with GPG, then store the GPG key in the repo <em>itself encrypted</em> with a GCP KMS key.</p><p>This is a common scheme that we use for secrets. We keep keys in GCP KMS that encrypt the secret values so that we can store the encrypted versions in the repo and then decrypt them during deploy time.</p><p>You might wonder then, "why not just encrypt the database files with a KMS key?". A totally valid question. As stated in <a href="https://cloud.google.com/kms/docs/envelope-encryption#balancing_deks_and_keks" title="the docs">the docs</a>, KMS has a 64 KiB limit for data encryption. Enough to encrypt a GPG key (creating a scheme known as 'envelope encryption'), but certainly not enough to encrypt a production database dump.</p><h2>Auto Deleting old Backups </h2><p>Auto deleting old backups was as easy as slapping a lifecycle rule on the storage bucket. Or at least, I thought it'd be. It turned out to be <em>slightly</em> trickier. </p><p>See, I use Terraform (as part of <a href="https://github.com/DevinSit/kubails" title="Kubails">Kubails</a>) to manage all my infrastructure. I figured adding a lifecycle rule to the storage bucket was just this:</p><pre><code>lifecycle_rule {
  condition {
    age = 60 # Days
  }

  action {
    type = "Delete"
  }
}</code></pre><p>This, in my mind, would enable deleting bucket objects that were older than 60 days.</p><p>Well, it turns out that using this rule wasn't enough. I don't know if this is GCP's fault or Terraform's, but a <em>second</em> condition ends up being set on the bucket: "Live State = Non-current".</p><p>This basically means that an object older than 60 days would only be deleted if it wasn't the 'current version' of the object. Except 'versions' only apply when you have object versioning turned on.</p><p>I didn't have object versioning turned on.</p><p>So this lifecycle rule actually did nothing.</p><p>I only realized this when, after a couple of days, the old backups in the bucket <em>still</em> hadn't been deleted.</p><p>So the 'correct' rule to apply is as follows:</p><pre><code>lifecycle_rule {
  condition {
    age = 60 # Days
    with_state = "LIVE"
  }

  action {
    type = "Delete"
  }
}</code></pre><p>Now 'live' objects (aka my normal objects) will be deleted.</p><p>This also turned out to be relevant for one of my other storage buckets (used for archiving logs) that had the same problem. It had been keeping around 1+ years' worth of logs even though they were supposed to be deleted after 6 months.</p><p>Goes to show how often I check the archives.</p><h2>AWS Backup Replication</h2><p>Copying backups to AWS was relatively simple. After I re-familiarized myself with how AWS authentication works (really, it's just access keys?), it was a simple matter of adding the AWS CLI to the Docker container that is used for the backup job (as in, the Kubernetes cronjob), injecting in the access key at run time, and copying the encrypted backup to an S3 bucket in addition to the usual GCS bucket.</p><p>The only other thing that was notable was my decision to use a separate Terraform config/state to handle AWS infrastructure (aka the single S3 bucket and some IAM stuff). Since all of the GCP Terraform infra was integrated into Kubails, I didn't want to 'taint' it with AWS. Plus, this was just simpler.</p><h2>Restoring Backups to New Namespaces</h2><p>As utterly cool and useful as this feature is, it wasn't actually that hard to set up. Essentially, all I had to do was modify a step in our build pipeline to fetch the latest (encrypted!) production backup, decrypt it, and then restore it to the newly created database in the newly created namespace.</p><p>Although, it was only so simple because I had previously done the work to make deploying the database a separate step of the build pipeline (so that we could also perform migrations separately from deploying the main services), so modifying that step was fairly straight forward.</p><p>And now that I've gotten to make use of this improvement, I must admit, per-branch database restores <em>is</em> really cool. And useful. Usefully cool.</p><div><hr></div><p>I <em>thought</em> this is what the extent of my DevOps improvements was gonna be, but it just kept snowballing from there...</p><p>Stay tuned for Part 2 of the DevOps Detour!</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #4]]></title><description><![CDATA[Walking but not Limping]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-4</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-4</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Mon, 29 Mar 2021 02:52:55 GMT</pubDate><content:encoded><![CDATA[<p>Ah <a href="https://ufincs.com">uFincs</a>, the only matter concerning my existence for the 4th post running (I wonder why).</p><h1>Last Time</h1><p>Hi there! Been a while since our <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-3">last time</a>. Was supposed to post an update a couple of weeks ago, but... ya know, things.</p><p>Anyways, last time, I was going on and on about mainly two things: Startup School and Recurring Transactions. My Startup School rants were mostly focused around their... 'deals', as well as my current progress on the Build Sprint. Whereas Recurring Transactions was just some behind-the-scenes rants.</p><p>And guess what? Today's more of the same. Sort of.</p><h1>Build Sprint</h1><p>"So... how'd the Build Sprint go?" you must surely <em>not</em> be wondering.</p><p>Well, I shall enlighten you regardless.</p><p>tl;dr Rather poorly.</p><p>To recap, I had set 3 goals:</p><ol><li><p>Launch publicly.</p></li><li><p>Acquire 2 customers.</p></li><li><p>Implement recurring transactions.</p></li></ol><h2>Launching Publicly</h2><p><em>Technically</em> speaking, I kinda launched publicly. That is, I formally enabled Stripe billing in production such that anybody who creates a new account must now subscribe upfront. </p><p>However, it's only "technically" because I didn't exactly publicise the news. I figured, since Recurring Transactions was well underway, I might as well wait for it to be finished before making such news public. However, as we'll see in a sec, it didn't really play out that way... (although one could argue that <em>this post</em> constitutes me making that news public, but it's not like anyone reads these things, right?).</p><h2>Acquiring Customers</h2><p>On point numero 2, I still have yet to acquire <em>any</em> paying customers. Reason is fairly simple: no one's gonna pay if no one knows about the service (refer to point 1). </p><p>Mhm, marketing.</p><h2>Recurring Transactions</h2><p>As for point 3, this was the real sticking point. Recurring Transactions ended up taking <em>slightly</em> longer than expected. Technically speaking, I would have liked to have had it done maybe a week before end-of-sprint so that I'd have time to start marketing uFincs around and acquiring our first customers, but realistically it would have taken till the end of the sprint to finish the feature. </p><p>In <em>reality</em>, it ended up taking an extra week past the end of the sprint. Which, all things considered, isn't the worst estimation I've ever made w.r.t. software development. But still, it threw off my schedule (and my mojo), so my Build Sprint was a failure.</p><p>That aside, at least it's good that Recurring Transactions are now done! At least to me, it's always been such a sticking point as far as the viability of uFincs, so finally getting it done and out of the way is a huge milestone. </p><p>It also helps that it was the biggest user-facing feature that I've implemented since the UI redesign. In fact, in terms of pull requests, I believe it was the single largest PR in terms of added lines specifically for one ticket: over 10,000. Mind you, 3k of those were from <code>package-lock.json</code>, but that's still a hefty amount. But it only makes sense considering the scope of the functionality.</p><h1>Other News</h1><p>In other news, I decided to heed some not-quite-a-customer-yet's advice and add an easy feedback form into the app so that people can complain directly to me, about the app, from the app.</p><p>In reality, beyond being a 'good thing to have' for UX, this is me having to compromise on the fact that I don't want to add much (if any) analytics to the in-app experience. If I don't want to track my users while they're using my app (as oh-so-many others do), then how am I supposed to know what's going?! As some might say, I'm "flying blind".</p><p>Well, I figure if someone is having problems, providing them an easy way to tell me about them is a "good enough" compromise. </p><p>"Social media", you say? </p><p>Never heard of it.</p><p>Anyways, the feedback form only took a couple of days to add, so no harm nor foul (in terms of scheduling).</p><h1>What's Next?</h1><p>Speaking of which, what's next on this oh-so-fickle schedule?</p><p>Well, now that Recurring Transactions (and the Feedback form) are complete, I suppose it's finally time to launch uFincs 'publicly'!</p><p>Honestly, I'm well past the point where I should have 'launched' uFincs, so I think it's time that I take a month and make launching/marketing my main focus. Good thing we've got a nice new month coming up soon!</p><p>I figure starting with the Startup School forums would be a good first place to 'launch'. The people over there are pretty damn receptive to new launches and giving feedback, so it should at least be a positive event. I don't really expect to get much traction, but the feedback/experience from just posting about uFincs should be good (side note: if it isn't obvious, let's just say I'm much more of a... lurker, when it comes to forums and whatnot).</p><p>After that? Well, I need to finally update my LinkedIn profile with my uFincs 'experience', so I'm sure I'll fire off a post over there. Again, I wouldn't really expect much traction; it is <em>LinkedIn</em> after all (although maybe the people in my network will be more intrigued to finally learn what I've been doing with my time).</p><p>And after <em>that</em>? Well, assuming uFincs doesn't dramatically take off or fall apart from bugs (aka my time is more needed for customer service or product work), then I'll probably start hitting up different subreddits or the Indie Hackers forums.</p><p>Inevitably, my first-phase marketing plan culminates with posting to Hacker News and Product Hunt. I would consider those the 'end-game' as far as 'forum acquisition' goes. </p><p>Once my forum targets have run dry, I can only <em>hope</em> I've generated some amount of traction. Maybe not necessarily the 100 customer goal I've set for the year, but at <em>least</em> 10 customers. If I can't even generate <em>that</em> level of traction after marketing to all these different forums, then I either <em>really</em> suck at writing compelling marketing material/choosing audiences, or maybe uFincs was a mistake after all.</p><p>We'll cross that bridge when we get there.</p><p>Till next time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #3]]></title><description><![CDATA[Sprinting Turns to Walking]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-3</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-3</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Mon, 01 Mar 2021 03:48:27 GMT</pubDate><content:encoded><![CDATA[<p>As the title should oh so obviously indicate, <a href="https://ufincs.com/">uFincs</a> is the matter concerning my existence today.</p><p>Oh boy.</p><h1>Last Time</h1><p><a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-2">Last time</a>, I regaled you with the tales of all the deals I was gonna get becoming 'Startup School certified' and participating in their so-called 'Build Sprint'.</p><p>I finished with an ominous question wondering how 'going to plan' everything would be at the time of this update.</p><p>Well, let's just say sprinting has turned to walking.</p><h1>The Startup School 'Deals'</h1><p>Last week, I finally participated in a 'group session' as part of the Startup School Build Sprint. Went well! Had some good discussions with some other founders to refine our pitches, had some technical discussions, good stuff.</p><p>Side note: I don't know if it was a coincidence or not, but all 3 of the other startup founders I talked with were German/in Germany. What are the chances?</p><p>Anywho, the more important part here is that participating in a group session finally gave me my coveted 'Startup School Certified' status, thus finally giving me access to their deals.</p><p>Well, they certainly had a list of deals... but the ones I was most interested in are either not a thing anymore or... well, let me explain.</p><h3>Stripe</h3><p>Let's start with Stripe. Followed through with Startup School's instructions on how to claim the deal: enter all your information into some form on Stripe's website (along with a provided 'partner code' that was presumably specific to Startup School). Got an email back from a Stripe sales representative shortly after with some follow-up questions to clarify if Stripe is a good fit for our business (you know, standard sales stuff).</p><p>Send back a reply and all I get back is a generic "Yup, looks like Stripe is a good fit for you! Here's the standard sign-up form, looking forward to taking your money!"</p><p>So... what happened to my deal? (For reference, the deal is advertised as no Stripe fees on the first $5000 in revenue processed). </p><p>Filling out Stripe's form with this special 'partner code' just seems to have triggered the standard sales process on Stripe's end. So I can only conclude that either the deal is no longer running or something has changed in terms of how to get the deal that isn't reflected accurately anymore.</p><p>Either way, I decided it wasn't really worth pursuing further (at 2.7% + 0.30 cents per charge, that's only like $150 in saved fees), so I'm just gonna forget about it, sign up for an account with my uFincs email, and move on with finishing the integration.</p><h3>GCP</h3><p>And then there was GCP. This deal was much more important for me since my infrastructure costs are the vast majority of my current monthly expenses. Not that it's that <em>much</em> (I currently spend around $50/month in infra), but it's still something for someone with no revenue.</p><p>Followed Startup School's instructions to a link that had changed (more out-of-date instructions...), but was quickly redirected to GCP's generic 'apply for startup credits page' (<a href="https://cloud.google.com/startup" title="Google Cloud for Startups | Google Cloud">Google Cloud for Startups | Google Cloud</a>).</p><p>Side note: I swear this page has changed in the past few months. I remember its call-to-action being to 'Contact your accelerator/investor' instead of the current 'Apply now' button. I guess they're now accepting general startups again?</p><p>Anyways, I fill out the form, list 'Startup School' as my 'investor' (reference?), and... nothing.</p><p>I got an email shortly after filling out the form saying that they'll schedule a call to discuss our 'needs' (aka, sales stuff) within 2 business days, but I haven't heard <em>anything</em> from them. No further emails, no calls, nada, nothing. </p><p>I guess this is just part-and-parcel to Google's <em>infamously bad</em> customer support (half /s). </p><p>Look, I love me some GCP (was formerly certified), but I can't say I'm surprised that I haven't heard back from them; they legitimately have a reputation for questionably non-existent support.</p><p>So yeah, can't say I'm expecting to get much out of this 'deal' either.</p><p>On the topic, I found it amusing to read through this Hacker News discussion on applying for AWS vs GCP credits: <a href="https://news.ycombinator.com/item?id=26252010" title="Google Cloud vs. AWS Onboarding Comparison | Hacker News">Google Cloud vs. AWS Onboarding Comparison | Hacker News</a>, along with the original article: <a href="https://www.kevinslin.com/notes/ebd7fd65-988f-422a-93f5-b1fe5c3f29ce.html" title="Google Cloud vs AWS Onboarding Comparison - Kevin's Page">Google Cloud vs AWS Onboarding Comparison - Kevin's Page</a>.</p><p>All-in-all, seems like dealing with Startup School <em>just</em> for access to their deals has been a waste of time so far. I'm sure some of their other deals have <em>got</em> to work, but the two I was particularly interested in have so far been busts. </p><p>Oh well. Doesn't really matter in the end; at least the content of Startup School (the video lectures and forums/community) has been quite good.</p><h1>Recurring Transactions</h1><p>The main technical goal of my Build Sprint &#8212; recurring transactions &#8212; are now well underway! Took quite a bit longer to get back into it (I haven't done much technical work on uFincs itself since Christmas), but I'm finally getting into the interesting parts of this feature.</p><p>That is, it was quite the slog to get through setting up all the data model/CRUD for recurring transactions. Lots of somewhat copy/pasting, a good bit of rather boring UI work, and a <em>ton</em> of boring form work. </p><p>Man, <code>react-hook-form</code> is great and all, but dealing with the 'super form' that is the transaction creation/editing form in uFincs can be quite the pain, what with its dozen+ inputs and another half-dozen conditional inputs.</p><p>Anyways, I'm now onto the part of how to actually handle 'realizing' recurring transactions. Both concretely (i.e. does this recurring transaction happen today? &#8594; create a regular transaction) and virtually (i.e. the user has browsed to 3 years into the future, how do we handle creating/displaying the recurring transactions that haven't actually happened yet?).</p><p>The virtual half is the more interesting half since users will theoretically be able to set up a bunch of recurring transactions and then 'project' out into the future what their finances will look like (aka, we're finally getting to the 'forecasting' part of uFincs). </p><p>This is interesting to implement both from the UI and datastore layers. UI, because we have to make sure all our widgets (i.e. tables, lists, and charts) support displaying these 'virtual' transactions. And data store, because what does it mean (from Redux's POV) to 'realize' 'virtual' transactions?</p><p>Anyways, those are the questions I'll be answering in the following weeks, so hopefully I can bring back those answers (or maybe even a working feature!) in the next update.</p><p>At my current publishing rate of once every 2 weeks, looks like my next update should coincide with the completion of the Build Sprint. This means I only have 2 weeks to launch publicly, acquire 2 customers, and finish recurring transactions.</p><p>Well, at my current rate, that is <em>definitely</em> not gonna happen. I've kinda been waffling on what I should be working on since I'm kinda pretty deep into recurring transactions at this point, but it probably makes more sense (from a business POV) to be focusing on the 'launch' (aka finishing the Stripe integration and then posting to the Startup School forums). </p><p>So yeah, I've probably mis-prioritized here, but oh well. Things are getting done; what difference does it really make <em>when</em> things are done?</p><p>Until next time.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #2]]></title><description><![CDATA[Sprinting Towards the Unknown]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-2</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-2</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Wed, 17 Feb 2021 03:12:58 GMT</pubDate><content:encoded><![CDATA[<p>The matter concerning my existence today is, once again, <a href="https://ufincs.com/">uFincs</a> &#8212; my lovely idea-turned-project-turned-company that is ever so slowly creeping towards becoming 'truly' public.</p><h1>Last Time</h1><p>When we last left off, I had just given you a wonderful introduction to what uFincs is, how it came to be, and what I was currently working on. You can check that out here: <a href="https://onmattersconcerningmyexistence.substack.com/p/ufincs-update-1" title="uFincs Update #1">uFincs Update #1</a>.</p><p>tl;dr Marketing site was in progress, preparing for a slow 'launch' to various groups to gather feedback (potential customers).</p><h1>This Time</h1><p>The marketing site has been finished! Check it out: <a href="https://ufincs.com/" title="uFincs | Privacy-first, Encrypted Personal Finance App">uFincs | Privacy-first, Encrypted Personal Finance App</a>.</p><p>However, there is still one caveat here. As much as the marketing site is now public, and as much as the app itself is now public, I'm not technically accepting customers yet. Why? Because billing is still disabled.</p><p>Why? Because I'm a cheap-ass and want to take advantage of whatever discounts or credits I can as I bootstrap this SaaS company. I have been participating in something known as <a href="https://www.startupschool.org/" title="Startup School">Startup School</a>, which is basically Y Combinator's (an accelerator) funnel into their main program. </p><p>One of the benefits of participating in Startup School is access to a set of deals that includes, among other things, a Stripe discount for something like no fees on the first $5000 processed. Pretty good! </p><p>However, to access the deals, you have to become 'Startup School Certified' &#8212; essentially, watch all their lecture videos, submit a weekly update 8 weeks in a row, and then join a group session.</p><p>I've completed the first two criteria, but I've yet to complete the third. Being the introvert I am, the prospect of talking about my product/company in front of strangers has been something that I've been putting off for as long as possible (I know, a terrible trait for a business owner/entrepreneur, but we deal).</p><p>Anyways, Startup School runs this 'Build Sprint' program that I've decided to participate in. As far as I can tell, there aren't any real <em>benefits</em> to participating in the program beyond just feedback and a platform for sharing (there used to be monetary grants, but those seem to be gone now). But I've decided to participate just to force myself to have to join the group sessions. </p><p>The timing works out well anyway. I'm in my preliminary launch phases, I need access to those deals, and the group sessions will be a good place to get feedback/to find potential customers. Win-win, as they say.</p><p>Once I have the Stripe deal and Stripe fully integrated, then that would be the point where we can 'officially' launch (aka start getting paid for all this work). I expect that to happen within the next couple of weeks &#8212; likely by the end of February.</p><p>Other than the logistics of launching (aka the business side), I've also been working on a new feature (aka the product side).</p><p>Recurring Transactions are now (legitimately) a work-in-progress! I say legitimately because I've actually had a section in the app for recurring transactions that was just a 'Work in Progress!' label. I mean, I had planned on it to happen ever since the redesign, but now that it's finally happening... feelsgoodman.</p><p>Oh yeah! Now that I've mentioned recurring transactions, this reminds me to write down my goals for the Startup School Build Sprint:</p><ul><li><p>Launch publicly.</p></li><li><p>Get 2 paying customers.</p></li><li><p>Implement recurring transactions.</p></li></ul><p>So I guess everything is going according to plan, for now.</p><p>We'll see how 'going to plan' everything is next update, scheduled for the end of the month.</p>]]></content:encoded></item><item><title><![CDATA[uFincs Update #1]]></title><description><![CDATA[Just putting this out there.]]></description><link>https://www.onmattersconcerningmyexistence.com/p/ufincs-update-1</link><guid isPermaLink="false">https://www.onmattersconcerningmyexistence.com/p/ufincs-update-1</guid><dc:creator><![CDATA[Devin Sit]]></dc:creator><pubDate>Fri, 29 Jan 2021 04:13:57 GMT</pubDate><content:encoded><![CDATA[<p>Today, there is a certain matter that concerns my existence that I gladly want to talk about: the status of my current project/company, <a href="https://ufincs.com/">uFincs</a>.</p><h1>Introducing: uFincs!</h1><p>For those unaware (aka, the vast majority of the human race), I've been working on this personal finance app called <a href="https://ufincs.com/">uFincs</a> for quite a while now &#8212; more than 2 years, if we count the time spent during university. Yes, university! uFincs was originally the capstone project for my Software Engineering degree; I decided to continue working on it post graduation, in hopes of polishing it enough that it could become a full-fledged product.</p><p>If I had to describe uFincs quickly, I'd say that it's a personal finance app where you manually track all of your transactions, never having to connect to your bank. It is very privacy friendly (hence, no bank connections), being end-to-end encrypted, while trying to make the process of manually keeping track of your finances as simple and easy as possible.</p><p>So why did I decide to build uFincs, when there are ostensibly a million-and-one other personal finance apps out there? Well, it mostly comes down to dissatisfaction: dissatisfaction with what was my existing personal finance solution and dissatisfaction with the other available solutions.</p><p>First, my original solution was GnuCash. A highly competent piece of software, for which I thoroughly enjoyed entering my transactions manually using double-entry accounting, but whose interface design was... dated, to say the least. Not to mention the mobile app was a crap-shoot, so entering things on my phone (aka, on the go) was a non-starter.</p><p>I really wanted to keep the spirit of GnuCash (hands-on finance management), but most every other personal finance solution on the market was one of two things:</p><ol><li><p>An aggregator that just connected to your bank account</p><ul><li><p>I hardly trust giving away my bank credentials to a third party, no matter how 'secure' they say they are.</p></li></ul></li><li><p>A budgeting app.</p><ul><li><p>I don't care for maintaining budgets. I just want to quickly enter transactions and be able to view their history, along with some stats applied over them.</p></li></ul></li></ol><p>There just really didn't seem to be a good app that handled entering transactions manually. Yes, some (even most) apps had such functionality, but they always seemed geared primarily to connecting to a bank, so such functionality always felt like an afterthought.</p><p>Of course, I could have just made my own spreadsheet (as many people have done), but I'm a software developer, damn it! There should be a better solution out there than a damn spreadsheet!</p><p>So yeah, as part of the capstone project for my degree, I decided to build my dream personal finance app: uFincs.</p><p>That's the general backstory behind uFincs just to catch you up, so now I want to talk about where uFincs is <em>today</em>.</p><h1>uFincs Today</h1><p>After a lot of work in 2020 (including a total UI redesign, finally adding e2e encryption, etc), uFincs now stands as... an un-launched product unknown to most and used only by me. However, that is all to change soon! The most recent work I've done is on building the marketing site for the app. Once that's finished, I intend on starting the 'launch' process by getting the app into the hands of a limited set of users (aka, the beta test).</p><p>Is this less than ideal? Well yes, of course. Ideally I should have been getting the app into people's hands during the entire development/redesign process. But I was intent on building something that I was, first and foremost, proud of. Of course, the fact that I just kinda wanted to work by myself, along with thinking that "I know best" certainly couldn't have helped...</p><p>However, now that I <em>am</em> quite proud of where uFincs is at, I'm finally starting to break out of my comfort zone to get this damn thing in front of other people. One step at a time, better late than never, etc.</p><p>The focus of 2021 will really be getting uFincs into people's hands (aka, applying the wonder that is <strong>marketing</strong>), and getting paying customers. I'm putting it on public record that my goal is to get 100 paying customers on any paid plan (monthly, annual, or lifetime) in 2021.</p><p>So yeah, keep an eye out on <a href="https://ufincs.com" title="ufincs.com">ufincs.com</a>. I'll definitely be posting here to let you know the progress on the way to the official public launch!</p>]]></content:encoded></item></channel></rss>