I’m so glad that I’ve maintained being a stickler about:
- Doing the work to systematize things, especially design assets
- Adhering to using Mac-ass Mac Apps unless absolutely necessary
I broke my wrist during Helene cleanup, so I’ve needed to get a Stream Deck to make up for lost time due to one-handed typing. Elgato’s default icon sets are very minimal, making it hard to get something readable quickly.
Buuuut because I’m using Sketch and I did all the work to make a design system around Wren’s branding work for Practical Computer, I was able to:
- Open a new sketch document
- Use the design system’s library
- Make a 96x96 icon artboard
- Plop a Sketch symbol in there
- Use overrides + Font Awesome’s local fonts to quickly churn out icons
They’re not perfect, bad even! But they’re readable, and it was a trivial amount of time, and shows the power of systematizing.
Nanoc continues to be one of the best tools I’ve ever learned. I built out a whole chain for Postmark template emails (mustache) using:
- Cerberus
- plain ol ERB for templates & layouts
- premailer to prep the pages for Emails, and generating plaintext versions
- unlimited previews using static JSON files, rendering the results for each variant
All with like, maybe 200 lines of code?
This gives me "checking the blinking lights on my Linksys router back when they still had the blue front face" nostalgia, thank you
I'm trying my best to make systems understandable. Years ago I read about how macOS (and software writ large) got rid of status bars, syncing/activity windows, and it stuck with me. So for Little CRM, I’m adding status lights to show if JS appears to be working, and if the Web Sockets connection was successful.
Every day is further proof that we simply need to use UTC across the board. No more timezones.
They have played us for absolute fools
I’ll have more to show and a proper writeup soon, but it’s extremely cool that LittleCRM has replies & threading, with realtime updates. And because it’s CableReady/mrujs, I was able to reuse a ton of code & keep the cognitive load to a minimum
JavaScript provides developers with a way to emit custom events that developers can listen for with the Element.addEventListener() method.
We can use custom events to let developers hook into the code that we write and run more code in response to when things happen. They provide a really flexible way to extend the functionality of a library or code base.
Another example of why UJS is so powerful: you start thinking in events.
Fixing the Rails networking stack is published
I just published ~3K words + diagrams + code samples + demo videos on the frontend networking stack I’m choosing for Little CRM, and all my apps going forward.
I know that’s a ton to dig into, but I believe this is a very strong path forward for resilient and maintainable web apps.
The idea of throwing out Turbo & going to CableReady + Mrujs might feel like it’s a step “backward," less tested, or that you’re forging your own path by not sticking with what Rails ships with.
But I’d argue the exact opposite. It uses what the browser gives us! The concepts & protocols are universal, not a bespoke framework! And it tees up your app to be resilient by default.
I am honestly worried about the cargo-culting around Turbo and how that’ll cause this to either be ratioed or downright dismissed. 🫣 I think there should be multiple options. I have philosophical disagreements with Turbo & see clear technical shortcomings that are hard to work around. But I’m also human, so I know I’m fallible.
I want a frontend stack for our community that can ultimately grow the ecosystem at large, help us attract & keep new blood, and build significantly better apps. From my perspective, this approach accomplishes all of that. I don’t need everyone to adopt it (I want that, because I believe in it 😜), but the overcorrection to “don’t do anything outside of what the framework provides” worries me. Especially on the frontend, given Rails’s history of jettisoning multiple iterations while never having sat down and truly thought out how the code lives on the client side.
I’m not saying this is the option, but it is an option. And it’s my strongly recommended option—the one I believe to be the most robust and easy to use.
Cat's a bit out of the bag on this one it seems 😅
I'm working on a detailed breakdown of Little CRM's networking stack, complete with real code samples and everyone's favorite: diagrams!
We don’t talk about technical ideas that have good aspects, but we don’t fully agree with. Everything is so focused on finding The One True Way. We love to talk “it depends” in our dayjob, but not our posts. Ultimately, it stifles creativity.
Jay Harris “I swear if you say ‘we have forgotten the old ways’ one more time [with regard to building sites that work without JS]”
Me: “this sounds more like a collective ‘we’ problem than a ‘me’ problem”
Y’all couldn’t put any sort of weighted algorithm on this? Like, I know almost all of these are rude in some capacity, but this just feels like a footgun waiting to happen
Little CRM is underway
After over a year of planning, chipping away at the problem, and designing the foundation for reuse; I’m finally starting on Little CRM.
The thing I’ve learned is that every time I decide the right thing engineering-wise, even if it’s just the bare-minimum version, I’m better off that it’s done.
I have found it; the saddest, unintentionally funniest podcast ad of all time
Blue Ridge Ruby 2024
I went to Blue Ridge Ruby, and it was an excellent time! I'm extremely grateful there's a regional Ruby conference (only a 90-minute drive away!) with great speakers and great company to boot.
My apprentice, Josh O'Halloran, got a sponsorship ticket and was immediately welcomed by the community. After 12+ years as a Rubyist, I had forgotten just how quickly & warmly we embrace newcomers. It's something we should continually practice.
Also, we need to make weird projects again! Talking about _why, Tenderlove, conference talks of yore, and other watershed moments in the early days of Ruby made me realize that we've been so focused on legitimizing, optimizing, and working with Ruby that we've stopped making dumb, silly things. If there's one critical thing I took away from my time in Asheville among folks that use the language I love every day, it's that we are better when we're collectively goofing off.
That, and root beer taught me the importance of B2B sales.
The lowest latency combination between Hetzner ARM and Crunchy Bridge
tl;dr: The best combination of Hetzner's ARM servers and Crunchy Bridge's regions are:
- Hetzner:
nbg1-dc3
- Crunchy Bridge: AWS
eu-central-1
- Benchmarks at the end of the post
I’ve been working on getting the first Practical Computer app out of beta (a dispatcher app for service companies). It, and the rest of our smaller apps, have the following constraints:
- Extremely shoestring budget. $30/month operating budget. Not much to work with!
- Since it’s production data, there’s responsibility for the data to be redundant, safe, and secure.
Given those, I ended up choosing Hetzner’s ARM servers, given the sheer power + cost combination (even with them being in the EU; and this is a US-based project).
I’ve had great experiences with the Crunchy Bridge team, and 18+ years of experience have taught me that I really want someone else to make sure the database is secure for me. So even though it’s 10% of my operating budget here, it’s worth it (because the database is the most crucial part of the app!).
The problem is that the app server & database are on two separate networks; I don’t get the benefit of an internal network, so I had to benchmark what combination of regions would work best.
The previous iteration was completely misconfigured: Hetzner’s Helsinki region for the app, AWS eu-north–1
for Crunchy Bridge. This turned out to be awful for performance. After some benchmarking and experimenting, I found that the following combination gets pretty dang close to good performance given the constraints:
- Hetzner:
nbg1-dc3
- Crunchy Bridge: AWS
eu-central–1
(I’ve got benchmarks at the end of the blog post, don’t worry!)
Lessons learned
I wanted to write this not only to help other folks find the best combination easily; but to talk about the mistakes I made and lessons I learned.
- Mistake: I should have benchmarked different options & done more research. My first Crunchy Bridge cluster was actually in
eu-central–1
, with the Hetzner server in Helsinki; and if I’d benchmarked & explored my options I would have avoided an unnecessary database migration. - Win: When spooling up the Hetzner server, I did my best to use Ansible to provision the server, and use Dokku for a PaaS-like experience. This made it so that spooling up a new server in
nbg1-dc3
was significantly easier. This is why I advocate for “automate as you go”, especially for infrastructure. - Win: choosing the right vendors. Hetzner allows me to quickly & cheaply spool up a small project, and having Crunchy Bridge’s support & their tooling made spooling up the replicas & promoting them dead-simple.
Alternative solves
To be clear, I had a few other options available to me, some of which I could still choose, and have implemented:
- Application-level caching: This is done, and basically a given for any production app. I ended up using Redis here.
- Making a read replica on the Hetzner instance; use that for reads (a heroically misguided, but technically feasible idea!)
- Move from Hetzner to AWS. This would only make sense for an app that’s actually generating the profit to justify the switch. It’s hard to bear Hetzner’s price & power combo here.
- Move the primary DB back to Hetzner (not ideal. Again, 18+ years of experience have taught me that I don’t want to manage a bunch of databases)
Benchmarks, as promised
# Benchmark script # x.report("crunchy-eu-central") { ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites }} # x.report("crunchy-eu-north-1") { ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } }hetzner-helsinki:~$ dokku run dispatcher rails@aaaaaa:/rails$ ruby script/benchmarks/database_performance.rb … ruby 3.3.1 (2024-04-23 revision c56cd86388) +YJIT [aarch64-linux] Warming up ————————————– crunchy-eu-central 1.000 i/100ms crunchy-eu-north-1 1.000 i/100ms Calculating ————————————- crunchy-eu-central 9.788 (±10.2%) i/s - 49.000 in 5.031111s crunchy-eu-north-1 14.124 (± 7.1%) i/s - 71.000 in 5.045164s
Comparison: crunchy-eu-north-1: 14.1 i/s crunchy-eu-central: 9.8 i/s - 1.44x slower
====================================================================================================================
hetzner-nuremberg:~$ dokku run dispatcher rails@5e9d1df43591:/rails$ ruby script/benchmarks/database_performance.rb … ruby 3.3.1 (2024-04-23 revision c56cd86388) +YJIT [aarch64-linux] Warming up ————————————– crunchy-eu-central 1.000 i/100ms crunchy-eu-north-1 7.000 i/100ms Calculating ————————————- crunchy-eu-central 20.452 (± 0.0%) i/s - 103.000 in 5.037650s crunchy-eu-north-1 96.340 (± 2.1%) i/s - 483.000 in 5.016063s
Comparison: crunchy-eu-north-1: 96.3 i/s crunchy-eu-central: 20.5 i/s - 4.71x slower
====================================================================================================================
/// Spot-check of query latency
hetzner-helsinki:~$ dokku run dispatcher rails@aaaaaa:/rails$ bin/rails c Loading production environment (Rails 7.1.3.2) irb(main):001> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites } } } 1637.3784099705517 103.8463581353426 106.55084392055869 105.82964192144573 102.70471591502428 102.40731597878039 107.70620591938496 112.95577604323626 111.48629407398403 105.25572090409696 => 10 irb(main):002> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } } } 1040.9399520140141 72.33657804317772 73.68614105507731 77.82750786282122 75.32874401658773 73.2035799883306 77.43134815245867 72.01353716664016 71.68629695661366 74.11726214922965 => 10
====================================================================================================================
hetzner-nuremberg:~$ dokku run dispatcher rails@aaaaaa:/rails$ bin/rails c Loading production environment (Rails 7.1.3.2) irb(main):001> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-central){ Organization.first.onsites } } } 846.7416610001237 54.84945299758692 56.269697000971064 55.53825500101084 54.511651000211714 54.32877099883626 57.52018099883571 59.374027001467766 63.36915899737505 55.06629299998167 => 10 irb(main):002> 10.times { p Benchmark.ms{ ApplicationRecord.connected_to(role: :crunchy-eu-north-1){ Organization.first.onsites } } } 168.30048900010297 11.614236002060352 12.266477999219205 15.882210002018837 15.326369000831619 11.819797000498511 14.219884997146437 10.650393000105396 11.742956001398852 13.63104199845111 => 10
Picked up the RadWagon over the weekend and have already checked off much of bike ownership: ✅ the gear I bought didn’t fit & have to reorder parts ✅ “it’ll just take a bit” maintenance turned into an hour+ and wheel disassembly to track down a mystery noise ✅ rained on the planned ride day ✅ immediately helped someone get their lost dog on my first test ride
Once again, Teams should ALWAYS be an MVP feature
One of the hardest parts of starting the codebase for the Practical Framework was the continual restraint I had to practice to do stuff that is boring & annoying, but I knew it needed full focus to get done right. Teams & Organizations were a prime example of that; because I knew academically that they should be an MVP feature
Frankly, it took like 1-2 months to get right; and slowed everything down. All boilerplate, nothing fun, no business logic. But imagine the sheer vindication felt when I got this email in the middle of one of the smallest betas I’ve ever worked on:
There is one thing I wanted to let you know, since I think [REDACTED] hasn’t spoken to you about this yet. […], is there a way that we can add a section to say which company has the job? So for example, I’ve been entering jobs for [X] but I know [REDACTED] wants to enter jobs into the dispatcher for [Y]
Teams/organizations are always always gonna show up
Finally got the copy “done” (in as much as anything is done on the web) for the Practical Computer site; and I’m really happy with how it all turned out! practical.computer
Famicom Disk System reproduction for transit cards from the Nintendo store in Kyoto + blank NFC Card + custom “meetup” landing page with contact form = digital business card for conferences.
I got the design for my Practical Computer business cards today and I can’t emphasize enough how much I love them. Can’t wait to see the final product
Use a PORO for Configuration
This post has been sitting in my drafts for months, so I'm shoving it out into the world!
Garrett Dimon and I have been chatting back & forth about Rails configurations. This is an extension/riff on his idea for Unified Configuration in Rails
While Garrett's approach hooks nicely into Rails and has multiple metaprogramming niceties; my approach is generalized & direct (but verbose). Essentially: create a Configuration
class that is a PORO; I named mine AppSettings
. You can see a gist of it here!
This is born out of a convention in my iOS apps, which use a Swift struct called Configuration
, that pulls the configuration from various sources into a strongly-typed, consistent location that is easy to remember.
My primary focus is to explicitly declare the configuration values and standardize how they’re accessed with a straightforward API, rather than try to standardize how they’re stored. This is because there are so many cases where either:
- “It depends” on how they should be stored.
- There’s an external dependency that makes you use a particular storage mechanism.
- The configuration is so entrenched as part of the environment that it ends up in the environment’s configuration.
Using a PORO with clearly defined methods gives you:
- Clarity in how the value is retrieved.
- The flexibility for different value types. Some are single keys, some are nested objects, etc.
- The same API for dynamically generated values; such as subdomained URIs
- An easy object to mock out for tests as needed.
I've been using this approach for an internal app I've been commissioned to make; it's worked out very well so far! I'd definitely recommend moving towards this approach in your projects.