<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mike Rispoli</title>
    <description>The latest articles on Forem by Mike Rispoli (@mrispoli24).</description>
    <link>https://forem.com/mrispoli24</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F143469%2F93acebe4-b716-4e24-910d-7b3930446edc.jpg</url>
      <title>Forem: Mike Rispoli</title>
      <link>https://forem.com/mrispoli24</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mrispoli24"/>
    <language>en</language>
    <item>
      <title>Land A $195K Job With AI</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Mon, 26 Jan 2026 15:52:11 +0000</pubDate>
      <link>https://forem.com/mrispoli24/land-a-195k-job-with-ai-5760</link>
      <guid>https://forem.com/mrispoli24/land-a-195k-job-with-ai-5760</guid>
      <description>&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/Bmuy1S2r5Yc"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;We applied for a $195K Treasury job using AI and got wildly different results.&lt;/p&gt;

&lt;p&gt;When Sam Corcos (&lt;a class="mentioned-user" href="https://dev.to/samcorcos"&gt;@samcorcos&lt;/a&gt;) posted a government IT specialist position requiring applicants to write a 10 page Great Gatsby analysis using AI, translate it into Spanish and Mandarin, and condense it into 200 words... we saw an opportunity to show you two completely different approaches to the same ambiguous problem.&lt;/p&gt;

&lt;p&gt;Mike (CTO) took the engineering route: Claude Code in terminal, planning phases, validation agents, citation checking, and systematic translation with dialect considerations.&lt;/p&gt;

&lt;p&gt;Justin (CEO) went full marketing: scrollable website, Roaring Twenties aesthetic, personality woven throughout, and creative ways to stand out in a sea of AI generated submissions.&lt;/p&gt;

&lt;p&gt;The truth? They're both testing the same thing. How do you break down an ambiguous task? How do you demonstrate your thought process? How do you use AI as a tool while showing your unique problem solving approach?&lt;/p&gt;

&lt;p&gt;This isn't just about landing a government job. It's about the future of interviews in an AI world. Show your work. Demonstrate your thinking. Stand out with your approach, not just your output.&lt;/p&gt;

&lt;p&gt;🔗 Original Job Post: &lt;a href="https://x.com/SamCorcos/status/201229" rel="noopener noreferrer"&gt;https://x.com/SamCorcos/status/201229&lt;/a&gt;...&lt;br&gt;
🔗 Job Listing: &lt;a href="https://www.usajobs.gov/job/854817200" rel="noopener noreferrer"&gt;https://www.usajobs.gov/job/854817200&lt;/a&gt;&lt;br&gt;
🔗 Justin's V0 Webpage: &lt;a href="https://v0.app/chat/gatsby-treasury-e" rel="noopener noreferrer"&gt;https://v0.app/chat/gatsby-treasury-e&lt;/a&gt;...&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapters:
&lt;/h2&gt;

&lt;p&gt;00:00 Intro: Applying for a $195K Government Job&lt;br&gt;
02:30 Breaking Down the Supplemental Assignment&lt;br&gt;
08:20 Mike's Technical Approach with Claude Code&lt;br&gt;
21:30 Planning Mode and Context Management&lt;br&gt;
32:00 Justin's Creative Marketing Approach&lt;br&gt;
46:00 Translation Challenges: Spanish and Mandarin&lt;br&gt;
54:00 Final Results: Engineering vs Marketing&lt;br&gt;
01:02:00 Key Lessons: Process Over Output&lt;/p&gt;

&lt;p&gt;Follow Sam Corcos: &lt;a class="mentioned-user" href="https://dev.to/samcorcos"&gt;@samcorcos&lt;/a&gt; on X&lt;br&gt;
Forward to Extraordinary.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claudecode</category>
      <category>claude</category>
      <category>jobs</category>
    </item>
    <item>
      <title>Installing DaisyUI in Rails without Node.js</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Wed, 19 Nov 2025 21:36:13 +0000</pubDate>
      <link>https://forem.com/mrispoli24/installing-daisyui-in-rails-without-nodejs-1eo0</link>
      <guid>https://forem.com/mrispoli24/installing-daisyui-in-rails-without-nodejs-1eo0</guid>
      <description>&lt;p&gt;I've been very intrigued by the no build frontend style of Rails 8+ and have begun a number of applications with a goal of sticking to this style. One area of difficulty though is that I love tailwind and namely DaisyUI. &lt;/p&gt;

&lt;p&gt;The standard install in a node project requires npm and some build scripting but I wanted to add this to my rails application without needing to introduce Node.js. The following is how I did that.&lt;/p&gt;

&lt;p&gt;I liked coupling this with the hotwire-spark gem to get hot reloading which gives the feeling of the hot reloading node frameworks I was used to but without the node for rails projects.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Rails 8+ with &lt;code&gt;tailwindcss-rails&lt;/code&gt; gem installed&lt;/li&gt;
&lt;li&gt;Tailwind CSS v4 configured&lt;/li&gt;
&lt;li&gt;Asset pipeline (Propshaft or Sprockets)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Installation Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Create Plugin Directory
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; app/assets/tailwind/plugins
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Download DaisyUI Standalone Module
&lt;/h3&gt;

&lt;p&gt;DaisyUI provides a standalone ESM module that can be used without npm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; app/assets/tailwind/plugins/daisyui.js https://cdn.jsdelivr.net/npm/daisyui@5/+esm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This downloads the complete DaisyUI library (~242KB) as a single JavaScript file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: The file is bundled with your application - no external CDN calls at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Configure Tailwind to Use DaisyUI Plugin
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;app/assets/tailwind/application.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./plugins/daisyui.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: The path is relative to the CSS file location.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Build Tailwind CSS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails tailwindcss:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see output confirming DaisyUI loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/*! 🌼 daisyUI 5.5.0 */
≈ tailwindcss v4.1.16
Done in 112ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You may see a warning about &lt;code&gt;@property --radialprogress&lt;/code&gt;. This is harmless - it's a newer CSS feature that DaisyUI uses for circular progress components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Using DaisyUI Components
&lt;/h2&gt;

&lt;p&gt;Once installed, all DaisyUI component classes are available:&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Alert Component
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"alert alert-success"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/svg"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"h-6 w-6 shrink-0 stroke-current"&lt;/span&gt; &lt;span class="na"&gt;fill=&lt;/span&gt;&lt;span class="s"&gt;"none"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 24 24"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;stroke-linecap=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-linejoin=&lt;/span&gt;&lt;span class="s"&gt;"round"&lt;/span&gt; &lt;span class="na"&gt;stroke-width=&lt;/span&gt;&lt;span class="s"&gt;"2"&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;Your purchase has been confirmed!&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: Card Component
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card bg-base-100 shadow-xl"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Card Title&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Card content goes here&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"card-actions justify-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Action&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: Form Components
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;label&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"label"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email_field&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"input input-bordered w-full"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-primary"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Submit&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Available DaisyUI Components
&lt;/h2&gt;

&lt;p&gt;DaisyUI provides 50+ components:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actions&lt;/strong&gt;: Button, Dropdown, Modal, Swap&lt;br&gt;
&lt;strong&gt;Data Display&lt;/strong&gt;: Alert, Badge, Card, Table, Avatar&lt;br&gt;
&lt;strong&gt;Data Input&lt;/strong&gt;: Input, Textarea, Checkbox, Radio, Select, Toggle, Range&lt;br&gt;
&lt;strong&gt;Layout&lt;/strong&gt;: Divider, Drawer, Footer, Hero, Navbar&lt;br&gt;
&lt;strong&gt;Navigation&lt;/strong&gt;: Breadcrumbs, Menu, Pagination, Tabs&lt;br&gt;
&lt;strong&gt;Feedback&lt;/strong&gt;: Loading, Progress, Toast&lt;/p&gt;

&lt;p&gt;Full list: &lt;a href="https://daisyui.com/components/" rel="noopener noreferrer"&gt;https://daisyui.com/components/&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Theming
&lt;/h2&gt;

&lt;p&gt;DaisyUI includes multiple themes. Configure in &lt;code&gt;app/assets/tailwind/application.css&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s1"&gt;"tailwindcss"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@plugin&lt;/span&gt; &lt;span class="s1"&gt;"./plugins/daisyui.js"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;/* Custom DaisyUI configuration */&lt;/span&gt;
&lt;span class="k"&gt;@layer&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="py"&gt;--color-primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;oklch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;45%&lt;/span&gt; &lt;span class="m"&gt;.24&lt;/span&gt; &lt;span class="m"&gt;277.023&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c"&gt;/* Custom purple */&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use built-in themes by adding to HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- Your app --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Available themes&lt;/strong&gt;: light, dark, cupcake, bumblebee, emerald, corporate, synthwave, retro, cyberpunk, valentine, halloween, garden, forest, aqua, lofi, pastel, fantasy, wireframe, black, luxury, dracula, cmyk, autumn, business, acid, lemonade, night, coffee, winter, dim, nord, sunset&lt;/p&gt;




&lt;h2&gt;
  
  
  Automatic Rebuilding in Development
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;tailwindcss-rails&lt;/code&gt; gem automatically watches for changes in development.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;Procfile.dev&lt;/code&gt; should include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;web: bin/rails server
css: bin/rails tailwindcss:watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When running &lt;code&gt;bin/dev&lt;/code&gt;, Tailwind automatically rebuilds when you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add new DaisyUI classes to views&lt;/li&gt;
&lt;li&gt;Modify Tailwind configuration&lt;/li&gt;
&lt;li&gt;Change CSS files&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Production Deployment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Precompile Assets
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RAILS_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;production bin/rails assets:precompile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles Tailwind CSS with DaisyUI into &lt;code&gt;app/assets/builds/tailwind.css&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker Deployment
&lt;/h3&gt;

&lt;p&gt;Include the DaisyUI plugin file in your image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/assets/tailwind ./app/assets/tailwind&lt;/span&gt;

&lt;span class="c"&gt;# Precompile assets&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;bin/rails tailwindcss:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiled CSS is self-contained - no runtime dependencies on DaisyUI or Tailwind.&lt;/p&gt;




&lt;h2&gt;
  
  
  Upgrading DaisyUI
&lt;/h2&gt;

&lt;p&gt;To upgrade to a newer version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download latest version&lt;/span&gt;
curl &lt;span class="nt"&gt;-o&lt;/span&gt; app/assets/tailwind/plugins/daisyui.js https://cdn.jsdelivr.net/npm/daisyui@latest/+esm

&lt;span class="c"&gt;# Rebuild&lt;/span&gt;
bin/rails tailwindcss:build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or pin to a specific version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-o&lt;/span&gt; app/assets/tailwind/plugins/daisyui.js https://cdn.jsdelivr.net/npm/daisyui@5.5.0/+esm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: "Unknown at rule: &lt;a class="mentioned-user" href="https://dev.to/plugin"&gt;@plugin&lt;/a&gt;"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Ensure you're using Tailwind CSS v4+. The &lt;code&gt;@plugin&lt;/code&gt; directive is new in v4.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue: DaisyUI classes not working
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solutions&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rebuild Tailwind: &lt;code&gt;bin/rails tailwindcss:build&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check the plugin path is correct in &lt;code&gt;application.css&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Verify &lt;code&gt;daisyui.js&lt;/code&gt; exists in &lt;code&gt;app/assets/tailwind/plugins/&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Issue: Styles not updating
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Restart &lt;code&gt;bin/dev&lt;/code&gt; to reload the Tailwind watcher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue: Large file size
&lt;/h3&gt;

&lt;p&gt;DaisyUI is ~242KB uncompressed. Tailwind CSS only includes classes you actually use in production, so the final CSS will be much smaller.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: NPM vs Standalone Method
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;NPM Method&lt;/th&gt;
&lt;th&gt;Standalone Method&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Node.js Required&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;npm/package.json&lt;/td&gt;
&lt;td&gt;✅ Yes&lt;/td&gt;
&lt;td&gt;❌ No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build Process&lt;/td&gt;
&lt;td&gt;Complex&lt;/td&gt;
&lt;td&gt;Simple&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline Deployment&lt;/td&gt;
&lt;td&gt;Requires node_modules&lt;/td&gt;
&lt;td&gt;✅ Fully self-contained&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Update Process&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; new version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File Size&lt;/td&gt;
&lt;td&gt;~242KB in node_modules&lt;/td&gt;
&lt;td&gt;~242KB in assets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Production Output&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both methods produce identical CSS output. Choose standalone for simplicity and self-contained deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DaisyUI Documentation&lt;/strong&gt;: &lt;a href="https://daisyui.com/" rel="noopener noreferrer"&gt;https://daisyui.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailwind CSS v4&lt;/strong&gt;: &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;https://tailwindcss.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tailwindcss-rails gem&lt;/strong&gt;: &lt;a href="https://github.com/rails/tailwindcss-rails" rel="noopener noreferrer"&gt;https://github.com/rails/tailwindcss-rails&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tailwindcss</category>
      <category>daisyui</category>
    </item>
    <item>
      <title>Setting Up Solid Cache on Heroku with a Single Database</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Tue, 11 Nov 2025 15:08:49 +0000</pubDate>
      <link>https://forem.com/mrispoli24/setting-up-solid-cache-on-heroku-with-a-single-database-49c5</link>
      <guid>https://forem.com/mrispoli24/setting-up-solid-cache-on-heroku-with-a-single-database-49c5</guid>
      <description>&lt;p&gt;I recently wanted to use Solid Cache for a new MVP I'm building out in Ruby on Rails. One thing I wanted was for this to all be deployed to a PaaS and in this case I was using Heroku. While I know Rails 8 pushes hard for Kamal and rolling your own deployment, for a lot of early projects it's nice to just have all the DevOps and CI/CD taken care of for you. &lt;/p&gt;

&lt;p&gt;This creates a problem when it comes to Solid Cache. Rails recommends running these with SQLite and in fact I have a production application using SQLite for everything that works amazing. However, Heroku is an ephemeral application server and as such, wipes out your SQLite stores on every deployment. &lt;/p&gt;

&lt;p&gt;Since this was an MVP I really just wanted to manage one database rather than introduce Redis or another instance of Postgres. After a lot of failed attempts and much googling, this was the solution I came up with.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;After deploying the Rails 8 application to Heroku, I encountered this error when trying to use rate limiting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PG::UndefinedTable (ERROR: relation "solid_cache_entries" does not exist)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This occurred because Rails 8's rate limiting feature depends on &lt;code&gt;Rails.cache&lt;/code&gt;, which in production is configured to use Solid Cache by default. However, the &lt;code&gt;solid_cache_entries&lt;/code&gt; table didn't exist in our database.&lt;/p&gt;

&lt;p&gt;This worked locally for me because in development Rails uses an in memory store so no database was required. It wasn't until deployment that I was able to see the error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Solid Cache
&lt;/h2&gt;

&lt;p&gt;Rails 8 introduces the "Solid" stack as default infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Solid Cache&lt;/strong&gt; - Database-backed cache store (replaces Redis/Memcached)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid Queue&lt;/strong&gt; - Database-backed job queue (replaces Sidekiq/Resque)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solid Cable&lt;/strong&gt; - Database-backed Action Cable (replaces Redis for WebSockets)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By default, Rails 8 expects these to use &lt;strong&gt;separate databases&lt;/strong&gt;. The &lt;code&gt;solid_cache:install&lt;/code&gt; generator creates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;config/cache.yml&lt;/code&gt; - Cache configuration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db/cache_schema.rb&lt;/code&gt; - Schema file (NOT a migration)&lt;/li&gt;
&lt;li&gt;Configuration pointing to a separate &lt;code&gt;cache&lt;/code&gt; database&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why Use a Single Database on Heroku?
&lt;/h2&gt;

&lt;p&gt;For our MVP, I chose to use a single PostgreSQL database for several reasons:&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost and Simplicity
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heroku provides one DATABASE_URL&lt;/strong&gt; - Additional databases cost extra&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simpler architecture&lt;/strong&gt; - Fewer moving parts during initial development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Easier to manage&lt;/strong&gt; - Single database connection, single backup strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Future Scalability Options
&lt;/h3&gt;

&lt;p&gt;When you outgrow this setup, you have clear upgrade paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis&lt;/strong&gt; - Better performance for high-traffic apps, separate caching layer&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate PostgreSQL database&lt;/strong&gt; - Isolate cache from primary data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed cache service&lt;/strong&gt; - Heroku Redis, AWS ElastiCache, etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Not SQLite for Cache on Heroku?
&lt;/h3&gt;

&lt;p&gt;While Solid Cache supports SQLite, &lt;strong&gt;Heroku's filesystem is ephemeral&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Files are wiped on dyno restart (at least once per 24 hours)&lt;/li&gt;
&lt;li&gt;Deployments create new dynos with fresh filesystems&lt;/li&gt;
&lt;li&gt;You'd lose all cached data frequently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SQLite-backed Solid Cache works great for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single-server VPS deployments (Kamal, Hetzner, DigitalOcean Droplets)&lt;/li&gt;
&lt;li&gt;Containerized apps with persistent volumes&lt;/li&gt;
&lt;li&gt;Development/staging environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for Heroku and similar PaaS platforms, use PostgreSQL or Redis for caching.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tried (And What Didn't Work)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Attempt 1: Running the Generator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails solid_cache:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Created &lt;code&gt;cache_schema.rb&lt;/code&gt; but no migration file. Changed &lt;code&gt;cache.yml&lt;/code&gt; to point to a separate &lt;code&gt;cache&lt;/code&gt; database that doesn't exist on Heroku.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 2: Official Multi-Database Setup
&lt;/h3&gt;

&lt;p&gt;Following the official Rails guides, I configured &lt;code&gt;database.yml&lt;/code&gt; with separate database entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;primary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV["DATABASE_URL"] %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV["DATABASE_URL"] %&amp;gt;&lt;/span&gt;  &lt;span class="c1"&gt;# Same database, different connection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;cache.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: The &lt;code&gt;cache_schema.rb&lt;/code&gt; file wasn't loaded by &lt;code&gt;db:migrate&lt;/code&gt; or &lt;code&gt;db:prepare&lt;/code&gt;. Rails expected separate databases with separate schema files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attempt 3: Using db:prepare
&lt;/h3&gt;

&lt;p&gt;Ran &lt;code&gt;bin/rails db:prepare&lt;/code&gt; hoping it would load all schema files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;: Only loaded &lt;code&gt;db/schema.rb&lt;/code&gt; (main migrations), ignored &lt;code&gt;db/cache_schema.rb&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Migration-Based Approach
&lt;/h2&gt;

&lt;p&gt;After researching (including &lt;a href="https://www.reddit.com/r/rails/comments/1gws1fp/help_needed_with_solid_cache/" rel="noopener noreferrer"&gt;this Reddit thread&lt;/a&gt;), I found the working solution for single-database Heroku deployments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Configure cache.yml for Single Database
&lt;/h3&gt;

&lt;p&gt;Remove the &lt;code&gt;database:&lt;/code&gt; configuration from production in &lt;code&gt;config/cache.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/cache.yml&lt;/span&gt;
&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default&lt;/span&gt;
  &lt;span class="na"&gt;store_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= 256.megabytes %&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= Rails.env %&amp;gt;&lt;/span&gt;

&lt;span class="na"&gt;development&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;

&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;*default&lt;/span&gt;  &lt;span class="c1"&gt;# No database: specified - uses primary connection&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important&lt;/strong&gt;: According to the &lt;a href="https://github.com/rails/solid_cache" rel="noopener noreferrer"&gt;Solid Cache README&lt;/a&gt;, when you omit &lt;code&gt;database&lt;/code&gt;, &lt;code&gt;databases&lt;/code&gt;, or &lt;code&gt;connects_to&lt;/code&gt; settings, Solid Cache automatically uses the &lt;code&gt;ActiveRecord::Base&lt;/code&gt; connection pool (your primary database).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Create a Migration for the Cache Table
&lt;/h3&gt;

&lt;p&gt;Generate a migration to create the solid_cache_entries table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate migration CreateSolidCacheEntries &lt;span class="nt"&gt;--database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--database=cache&lt;/code&gt; flag keeps the migration organized (though it still runs against the primary database in our single-DB setup).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Copy Table Definition from cache_schema.rb
&lt;/h3&gt;

&lt;p&gt;Update the generated migration with the exact table structure from &lt;code&gt;db/cache_schema.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# db/migrate/YYYYMMDDHHMMSS_create_solid_cache_entries.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateSolidCacheEntries&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;8.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:solid_cache_entries&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;536870912&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:key_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt; &lt;span class="ss"&gt;:byte_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;

      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:byte_size&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:key_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:byte_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:key_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Run Migration Locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the table was created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails runner &lt;span class="s2"&gt;"puts ActiveRecord::Base.connection.table_exists?('solid_cache_entries')"&lt;/span&gt;
&lt;span class="c"&gt;# Should output: true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Keep Development Simple
&lt;/h3&gt;

&lt;p&gt;Leave development environment using &lt;code&gt;:memory_store&lt;/code&gt; in &lt;code&gt;config/environments/development.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:memory_store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the Rails convention and keeps development simple. Production uses Solid Cache, development uses in-memory caching.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Deploy to Heroku
&lt;/h3&gt;

&lt;p&gt;Your existing &lt;code&gt;Procfile&lt;/code&gt; with the release phase will handle the migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;release: bundle exec rails db:migrate
web: bundle exec puma -C config/puma.rb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Deploy and the migration runs automatically during the release phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;After deployment, verify Solid Cache is working:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check Heroku logs&lt;/strong&gt; during release phase - should see migration run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test rate limiting&lt;/strong&gt; - Try to trigger rate limits on login endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check cache in Rails console&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;   &lt;span class="c1"&gt;# On Heroku&lt;/span&gt;
   &lt;span class="n"&gt;heroku&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt; &lt;span class="n"&gt;rails&lt;/span&gt; &lt;span class="n"&gt;console&lt;/span&gt;

   &lt;span class="c1"&gt;# Test cache&lt;/span&gt;
   &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test_key'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'test_value'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'test_key'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Should return 'test_value'&lt;/span&gt;

   &lt;span class="c1"&gt;# Check table&lt;/span&gt;
   &lt;span class="no"&gt;SolidCache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;  &lt;span class="c1"&gt;# Should be &amp;gt; 0 if cache is working&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cache Expiration and Cleanup
&lt;/h2&gt;

&lt;p&gt;Solid Cache includes automatic cleanup to prevent indefinite growth. Our configuration uses both size and age-based expiration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/cache.yml&lt;/span&gt;
&lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nl"&gt;&amp;amp;default&lt;/span&gt;
  &lt;span class="na"&gt;store_options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;max_age&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= 30.days.to_i %&amp;gt;&lt;/span&gt;     &lt;span class="c1"&gt;# Delete entries older than 30 days&lt;/span&gt;
    &lt;span class="na"&gt;max_size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= 256.megabytes %&amp;gt;&lt;/span&gt;   &lt;span class="c1"&gt;# Delete oldest when exceeds 256MB&lt;/span&gt;
    &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= Rails.env %&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How Automatic Cleanup Works
&lt;/h3&gt;

&lt;p&gt;Solid Cache uses a &lt;strong&gt;write-triggered background thread&lt;/strong&gt; (not a separate job system):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write tracking&lt;/strong&gt; - Every cache write increments an internal counter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic activation&lt;/strong&gt; - After ~50 writes, cleanup runs on a background thread&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cleanup logic&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;max_size&lt;/code&gt; exceeded → Delete oldest entries (LRU eviction)&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;max_age&lt;/code&gt; set and size OK → Delete entries older than max_age&lt;/li&gt;
&lt;li&gt;Deletes in batches of 100 entries (&lt;code&gt;expiry_batch_size&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL-based deletion&lt;/strong&gt; - Runs standard SQL DELETE queries:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;   &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;solid_cache_entries&lt;/span&gt;
   &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'30 days'&lt;/span&gt;
   &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Important Characteristics
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;No Solid Queue required&lt;/strong&gt; - Uses built-in Ruby threads, not Active Job&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;No cron jobs needed&lt;/strong&gt; - Self-managing and automatic&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Database-agnostic&lt;/strong&gt; - Pure SQL, works with any ActiveRecord adapter&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Efficient&lt;/strong&gt; - Background thread idles when cache isn't being written to&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Write-dependent&lt;/strong&gt; - Cleanup only triggers when cache receives writes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For rate limiting (which writes on every login attempt), this mechanism works perfectly and requires no additional infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why 30 Days for Rate Limiting?
&lt;/h3&gt;

&lt;p&gt;Rate limiting data is inherently short-lived:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate limit windows are 3 minutes&lt;/li&gt;
&lt;li&gt;Session data expires in days, not months&lt;/li&gt;
&lt;li&gt;Old cache entries from expired sessions serve no purpose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;30 days is generous for this use case and prevents cache bloat while maintaining safety margins.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rails 8's default Solid Stack assumes multi-database setup&lt;/strong&gt; - This doesn't match Heroku's single DATABASE_URL model&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema files aren't migrations&lt;/strong&gt; - &lt;code&gt;db/cache_schema.rb&lt;/code&gt; won't be loaded by &lt;code&gt;db:migrate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Omitting &lt;code&gt;database:&lt;/code&gt; in cache.yml uses the primary connection&lt;/strong&gt; - This is the key for single-database setups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create a regular migration&lt;/strong&gt; - Convert the schema file to a migration for single-database deployments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite doesn't work on ephemeral filesystems&lt;/strong&gt; - Use PostgreSQL or Redis for caching on Heroku&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development can use :memory_store&lt;/strong&gt; - No need to complicate local development&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic cleanup is built-in&lt;/strong&gt; - Solid Cache handles expiration via background threads, no Solid Queue or cron jobs required&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Future Migration Path
&lt;/h2&gt;

&lt;p&gt;When your app scales and you need better cache performance:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Add Heroku Redis&lt;/strong&gt; (~$15/month for hobby tier)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update production.rb&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;   &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cache_store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;:redis_cache_store&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;url: &lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'REDIS_URL'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Remove Solid Cache dependency&lt;/strong&gt; if desired, or keep for other purposes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The migration is straightforward and won't require code changes beyond configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://guides.rubyonrails.org/caching_with_rails.html" rel="noopener noreferrer"&gt;Rails Caching Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/rails/solid_cache" rel="noopener noreferrer"&gt;Solid Cache GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.reddit.com/r/rails/comments/1gws1fp/help_needed_with_solid_cache/" rel="noopener noreferrer"&gt;Reddit: Solid Cache single database discussion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devcenter.heroku.com/articles/getting-started-with-rails8" rel="noopener noreferrer"&gt;Heroku Rails Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>heroku</category>
      <category>solidcache</category>
    </item>
    <item>
      <title>Setting up an encrypted secondary drive on Linux</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Mon, 10 Nov 2025 15:15:30 +0000</pubDate>
      <link>https://forem.com/mrispoli24/setting-up-an-encrypted-secondary-drive-on-linux-3ioa</link>
      <guid>https://forem.com/mrispoli24/setting-up-an-encrypted-secondary-drive-on-linux-3ioa</guid>
      <description>&lt;p&gt;I recently received my brand new maxed out Framework Desktop and immediately installed Omarchy on it. However, I purchased a secondary drive and realized I had never actually had a second drive in my machine before. I went down the rabbit hole of setting this one up with full disk encryption and automatic mounting so you don't have to.&lt;/p&gt;

&lt;p&gt;Spoiler alert, Claude code helped me with a lot of this, however I wanted to have a complete breakdown of everything we did so that I could a. reference it later and b. double check all of the things it was telling me to do so I didn't brick my machine. What follows is the exact setup I am using. My secondary drive is accessible from my home directory, is encrypted just like my main drive, mounts on startup, and doesn't require me to enter a password for this drive on every restart.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Introduction&lt;/li&gt;
&lt;li&gt;What We're Doing&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;
Step-by-Step Guide

&lt;ul&gt;
&lt;li&gt;Step 1: Partitioning the Drive&lt;/li&gt;
&lt;li&gt;Step 2: Setting Up LUKS Encryption&lt;/li&gt;
&lt;li&gt;Step 3: Creating a Key File for Auto-Unlock&lt;/li&gt;
&lt;li&gt;Step 4: Formatting with btrfs&lt;/li&gt;
&lt;li&gt;Step 5: Mounting and Setting Permissions&lt;/li&gt;
&lt;li&gt;Step 6: Configuring Auto-Mount at Boot&lt;/li&gt;
&lt;li&gt;Step 7: Testing the Configuration&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Understanding the Final Setup&lt;/li&gt;

&lt;li&gt;Troubleshooting&lt;/li&gt;

&lt;li&gt;Useful Commands&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This guide walks you through setting up a secondary hard drive on Omarch Linux (an Arch-based distribution) running on a Framework Desktop. The setup includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full disk encryption using LUKS (Linux Unified Key Setup)&lt;/li&gt;
&lt;li&gt;btrfs filesystem (same as your main drive)&lt;/li&gt;
&lt;li&gt;Automatic unlocking and mounting at boot&lt;/li&gt;
&lt;li&gt;Proper permissions for your user account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time required:&lt;/strong&gt; ~15-20 minutes&lt;br&gt;
&lt;strong&gt;Difficulty:&lt;/strong&gt; Intermediate (but explained for beginners!)&lt;br&gt;
&lt;strong&gt;Risk:&lt;/strong&gt; Low (we're only working with the secondary drive, not your main system drive)&lt;/p&gt;


&lt;h2&gt;
  
  
  What We're Doing
&lt;/h2&gt;

&lt;p&gt;Your Framework Desktop has two 2TB NVMe drives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary drive&lt;/strong&gt; (&lt;code&gt;/dev/nvme0n1&lt;/code&gt;): Contains your operating system and home directory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary drive&lt;/strong&gt; (&lt;code&gt;/dev/nvme1n1&lt;/code&gt;): Empty drive we'll set up for data storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll set up the secondary drive to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Be encrypted (like your main drive) for security&lt;/li&gt;
&lt;li&gt;Automatically unlock when you boot (using a key file stored on your encrypted main drive)&lt;/li&gt;
&lt;li&gt;Mount at &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt; so you can easily access it&lt;/li&gt;
&lt;li&gt;Use the btrfs filesystem for modern features like snapshots and compression&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Why this approach?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Security:&lt;/strong&gt; Your data is encrypted, so if someone physically removes the drive, they can't access it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convenience:&lt;/strong&gt; You only enter one password at boot (for your main drive), and the secondary drive unlocks automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency:&lt;/strong&gt; Using the same filesystem (btrfs) as your main drive keeps things familiar&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before starting, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[x] Admin (sudo) access to your system&lt;/li&gt;
&lt;li&gt;[x] Your user account password&lt;/li&gt;
&lt;li&gt;[x] At least 10-15 minutes of uninterrupted time&lt;/li&gt;
&lt;li&gt;[x] A backup of any data currently on the secondary drive (we'll erase everything!)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check your sudo access:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should return &lt;code&gt;root&lt;/code&gt;. If you get an error, you may need to fix your sudo configuration first.&lt;/p&gt;

&lt;p&gt;In my case, with a fresh installation of Omarchy, I had to run &lt;code&gt;omarchy-reset-sudo&lt;/code&gt; to fix it as per &lt;a href="https://github.com/basecamp/omarchy/issues/1571" rel="noopener noreferrer"&gt;this issue&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step Guide
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Partitioning the Drive
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is partitioning?&lt;/strong&gt;&lt;br&gt;
Think of partitioning like dividing a physical hard drive into separate sections. Even though we have one physical drive, we can create multiple partitions that act like separate drives. For this setup, we'll create one large partition that uses the entire drive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is GPT?&lt;/strong&gt;&lt;br&gt;
GPT (GUID Partition Table) is the modern partitioning scheme that replaced the older MBR (Master Boot Record). It's required for drives larger than 2TB and works better with modern UEFI systems (which your Framework Desktop uses).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;parted /dev/nvme1n1 &lt;span class="nt"&gt;--script&lt;/span&gt; mklabel gpt mkpart primary 0% 100%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking it down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sudo&lt;/code&gt;: Run this command with administrator privileges&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;parted&lt;/code&gt;: The partitioning tool we're using&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dev/nvme1n1&lt;/code&gt;: The device name for your second NVMe drive

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/dev/&lt;/code&gt; is where Linux stores device files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nvme1n1&lt;/code&gt; means NVMe drive #1 (counting from 0, so 0 is first, 1 is second)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;--script&lt;/code&gt;: Run in non-interactive mode (don't ask for confirmation)&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;mklabel gpt&lt;/code&gt;: Create a new GPT partition table&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;mkpart primary 0% 100%&lt;/code&gt;: Create a primary partition using 0% to 100% of the drive (the whole thing)&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Verify it worked:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk /dev/nvme1n1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should now see &lt;code&gt;/dev/nvme1n1p1&lt;/code&gt; (the "p1" means "partition 1").&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Setting Up LUKS Encryption
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is LUKS?&lt;/strong&gt;&lt;br&gt;
LUKS (Linux Unified Key Setup) is the standard encryption system for Linux. It encrypts your entire partition, making the data unreadable without the correct password (passphrase). Even if someone physically steals your drive, they can't access the data without your passphrase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksFormat /dev/nvme1n1p1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking it down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cryptsetup&lt;/code&gt;: The tool for managing encrypted volumes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;luksFormat&lt;/code&gt;: Initialize LUKS encryption on a partition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dev/nvme1n1p1&lt;/code&gt;: The partition we just created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens when you run this:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You'll see a warning that this will erase all data (type &lt;code&gt;YES&lt;/code&gt; in all caps to confirm)&lt;/li&gt;
&lt;li&gt;You'll be asked to create a passphrase&lt;/li&gt;
&lt;li&gt;You'll need to enter it twice to confirm&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Important notes about the passphrase:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a &lt;strong&gt;backup unlock method&lt;/strong&gt; - you won't need it for daily use (we'll set up automatic unlocking next)&lt;/li&gt;
&lt;li&gt;Write it down and store it somewhere safe!&lt;/li&gt;
&lt;li&gt;If you lose both this passphrase AND the key file we create later, your data is permanently lost&lt;/li&gt;
&lt;li&gt;Make it strong but memorable (passphrase example: "correct horse battery staple")&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 3: Creating a Key File for Auto-Unlock
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is a key file?&lt;/strong&gt;&lt;br&gt;
A key file is a file containing random data that acts like a password. Instead of typing a passphrase every time you boot, the system reads this file automatically. Since this file is stored on your encrypted main drive, it's protected by your main drive's encryption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Think of it like this:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your main drive is a locked safe (encrypted with your boot password)&lt;/li&gt;
&lt;li&gt;Inside that safe, you store a key to another safe (the key file for your secondary drive)&lt;/li&gt;
&lt;li&gt;When you unlock the first safe, you automatically have access to the key for the second safe&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Commands:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create a secure directory for key files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /root/keyfiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkdir&lt;/code&gt;: Make directory&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-p&lt;/code&gt;: Create parent directories if needed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/root/keyfiles&lt;/code&gt;: Location in the root user's home directory (only accessible by root)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generate a random key file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo dd &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/urandom &lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/root/keyfiles/storage.key &lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1024 &lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dd&lt;/code&gt;: "Data duplicator" - copies data from one place to another&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if=/dev/urandom&lt;/code&gt;: Input file is &lt;code&gt;/dev/urandom&lt;/code&gt; (a source of random data)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;of=/root/keyfiles/storage.key&lt;/code&gt;: Output file (where to save the key)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bs=1024&lt;/code&gt;: Block size of 1024 bytes (1 kilobyte)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;count=4&lt;/code&gt;: Create 4 blocks (total size: 4KB of random data)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Secure the key file permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /root/keyfiles/storage.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chmod&lt;/code&gt;: Change file mode (permissions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;600&lt;/code&gt;: Only the owner (root) can read and write; no one else can access it

&lt;ul&gt;
&lt;li&gt;First digit (6): Owner can read (4) + write (2) = 6&lt;/li&gt;
&lt;li&gt;Second digit (0): Group has no permissions&lt;/li&gt;
&lt;li&gt;Third digit (0): Others have no permissions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Add the key file to LUKS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksAddKey /dev/nvme1n1p1 /root/keyfiles/storage.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;luksAddKey&lt;/code&gt;: Add a new way to unlock this encrypted partition&lt;/li&gt;
&lt;li&gt;You'll be asked for the passphrase you created in Step 2 (to prove you're authorized to add a new key)&lt;/li&gt;
&lt;li&gt;After this, the drive can be unlocked either with the passphrase OR the key file&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 4: Formatting with btrfs
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is a filesystem?&lt;/strong&gt;&lt;br&gt;
A filesystem determines how data is organized and stored on a partition. Think of it like the filing system in a library - it defines how books (files) are organized, cataloged, and retrieved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why btrfs?&lt;/strong&gt;&lt;br&gt;
Btrfs (B-tree File System, pronounced "butter fs" or "better fs") is a modern filesystem with advanced features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Snapshots:&lt;/strong&gt; Take instant backups of your entire drive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compression:&lt;/strong&gt; Automatically compress files to save space&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data integrity:&lt;/strong&gt; Checks for and repairs corruption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subvolumes:&lt;/strong&gt; Create separate "sub-filesystems" within one partition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your main Omarch drive uses btrfs, so using it for your secondary drive keeps things consistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unlock (open) the encrypted partition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup open /dev/nvme1n1p1 storage &lt;span class="nt"&gt;--key-file&lt;/span&gt; /root/keyfiles/storage.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cryptsetup open&lt;/code&gt;: Unlock an encrypted partition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dev/nvme1n1p1&lt;/code&gt;: The encrypted partition&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt;: The name to give this unlocked device (it will appear as &lt;code&gt;/dev/mapper/storage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--key-file&lt;/code&gt;: Use the key file instead of prompting for a passphrase&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Format with btrfs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mkfs.btrfs &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="s2"&gt;"Storage"&lt;/span&gt; /dev/mapper/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mkfs.btrfs&lt;/code&gt;: Make a btrfs filesystem&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-L "Storage"&lt;/code&gt;: Give it a label called "Storage" (appears in file managers)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dev/mapper/storage&lt;/code&gt;: The unlocked encrypted partition&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What you'll see:&lt;/strong&gt;&lt;br&gt;
The command outputs information about your new filesystem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;UUID:&lt;/strong&gt; A unique identifier for this filesystem&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node size:&lt;/strong&gt; How btrfs organizes metadata (16KB is standard)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filesystem size:&lt;/strong&gt; Total capacity (1.82TB)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSD detected:&lt;/strong&gt; Btrfs optimizes for SSD/NVMe drives automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features:&lt;/strong&gt; Modern btrfs features enabled&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  Step 5: Mounting and Setting Permissions
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What is mounting?&lt;/strong&gt;&lt;br&gt;
Mounting is the process of making a filesystem accessible at a specific location in your directory tree. Think of it like assigning a drive letter in Windows (C:, D:, etc.), except in Linux you can mount drives anywhere in your folder structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Create the mount point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Creates a directory at &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt; where we'll access the drive&lt;/li&gt;
&lt;li&gt;This is just an empty folder right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Mount the filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/mapper/storage /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;mount&lt;/code&gt;: Make a filesystem accessible&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/dev/mapper/storage&lt;/code&gt;: The unlocked encrypted drive&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;: Where to mount it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now when you access &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;, you're actually reading/writing to the encrypted drive!&lt;/p&gt;

&lt;p&gt;Set ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &amp;lt;your_username&amp;gt;:&amp;lt;your_username&amp;gt; /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chown&lt;/code&gt;: Change ownership&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;your_username&amp;gt;:&amp;lt;your_username&amp;gt;&lt;/code&gt;: User '' and group '' (your username)&lt;/li&gt;
&lt;li&gt;Without this, only root could write to the drive&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Step 6: Configuring Auto-Mount at Boot
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The goal:&lt;/strong&gt; Make the drive unlock and mount automatically when you boot your computer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two configuration files are involved:&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  /etc/crypttab (Crypto Tab)
&lt;/h4&gt;

&lt;p&gt;This file tells the system which encrypted partitions to unlock at boot and how to unlock them.&lt;/p&gt;

&lt;h4&gt;
  
  
  /etc/fstab (File System Tab)
&lt;/h4&gt;

&lt;p&gt;This file tells the system which filesystems to mount at boot and where to mount them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Get the UUID of the encrypted partition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;blkid /dev/nvme1n1p1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;blkid&lt;/code&gt;: Block device ID - shows information about partitions&lt;/li&gt;
&lt;li&gt;You'll see output like: &lt;code&gt;UUID="b57373d9-72d9-4ec7-9354-8f3cd61d3df4" TYPE="crypto_LUKS"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy the UUID (the long string of numbers and letters)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add to /etc/crypttab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"storage UUID=b57373d9-72d9-4ec7-9354-8f3cd61d3df4 /root/keyfiles/storage.key luks"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/crypttab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking it down:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;echo&lt;/code&gt;: Output text&lt;/li&gt;
&lt;li&gt;The text is: &lt;code&gt;storage UUID=YOUR-UUID /root/keyfiles/storage.key luks&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;storage&lt;/code&gt;: Name for the unlocked device (will appear as &lt;code&gt;/dev/mapper/storage&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;UUID=...&lt;/code&gt;: Identifies which encrypted partition to unlock&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/root/keyfiles/storage.key&lt;/code&gt;: Path to the key file to use for unlocking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;luks&lt;/code&gt;: This is a LUKS-encrypted partition&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;|&lt;/code&gt;: Pipe - send the output to the next command&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;sudo tee -a /etc/crypttab&lt;/code&gt;: Append to the /etc/crypttab file

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;tee&lt;/code&gt;: Write to a file and also show output&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-a&lt;/code&gt;: Append (don't overwrite)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Add to /etc/fstab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"/dev/mapper/storage /home/&amp;lt;your_username&amp;gt;/storage btrfs defaults 0 2"&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking it down:&lt;/strong&gt;&lt;br&gt;
The text is: &lt;code&gt;/dev/mapper/storage /home/&amp;lt;your_username&amp;gt;/storage btrfs defaults 0 2&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/dev/mapper/storage&lt;/code&gt;: The device to mount (your unlocked encrypted drive)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;: Where to mount it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;btrfs&lt;/code&gt;: The filesystem type&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;defaults&lt;/code&gt;: Use default mount options (read-write, auto-mount, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt;: Don't dump (used by backup tools, 0 means skip)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt;: Filesystem check order (0=don't check, 1=check first, 2=check after root)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens at boot now:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;System reads /etc/crypttab&lt;/li&gt;
&lt;li&gt;Unlocks &lt;code&gt;/dev/nvme1n1p1&lt;/code&gt; using the key file, creating &lt;code&gt;/dev/mapper/storage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;System reads /etc/fstab&lt;/li&gt;
&lt;li&gt;Mounts &lt;code&gt;/dev/mapper/storage&lt;/code&gt; at &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Your drive is ready to use!&lt;/li&gt;
&lt;/ol&gt;


&lt;h3&gt;
  
  
  Step 7: Testing the Configuration
&lt;/h3&gt;

&lt;p&gt;Before rebooting, let's test that everything works correctly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Commands:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unmount and close the drive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /home/&amp;lt;your_username&amp;gt;/storage
&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup close storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This simulates the state before boot (drive locked and unmounted)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reload systemd and test auto-mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl daemon-reload
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl restart cryptsetup.target
&lt;span class="nb"&gt;sudo &lt;/span&gt;mount &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;systemctl daemon-reload&lt;/code&gt;: Reload systemd configuration files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemctl restart cryptsetup.target&lt;/code&gt;: Trigger encryption unlock (reads /etc/crypttab)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mount -a&lt;/code&gt;: Mount all filesystems in /etc/fstab that aren't already mounted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Verify it's mounted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;df -h&lt;/code&gt;: Show disk free space in human-readable format&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;| grep storage&lt;/code&gt;: Filter to only show lines containing "storage"&lt;/li&gt;
&lt;li&gt;You should see &lt;code&gt;/dev/mapper/storage&lt;/code&gt; mounted at &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Test that you can write to it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Test file - LLMs will go here!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/test.txt
&lt;span class="nb"&gt;cat&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Creates a test file and reads it back&lt;/li&gt;
&lt;li&gt;If this works, your permissions are correct!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Understanding the Final Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What You Have Now
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Physical layout:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/dev/nvme1n1 (2TB NVMe drive)
└── /dev/nvme1n1p1 (2TB partition, LUKS encrypted)
    └── /dev/mapper/storage (unlocked encrypted partition, btrfs filesystem)
        └── /home/&amp;lt;your_username&amp;gt;/storage (mounted and accessible)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;At boot:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You enter your main drive password&lt;/li&gt;
&lt;li&gt;System unlocks your main drive (containing your OS and the key file)&lt;/li&gt;
&lt;li&gt;System reads &lt;code&gt;/root/keyfiles/storage.key&lt;/code&gt; from your main drive&lt;/li&gt;
&lt;li&gt;System automatically unlocks &lt;code&gt;/dev/nvme1n1p1&lt;/code&gt; using the key file&lt;/li&gt;
&lt;li&gt;System mounts it at &lt;code&gt;/home/&amp;lt;your_username&amp;gt;/storage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ready to use!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Security model:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Both drives are encrypted&lt;/li&gt;
&lt;li&gt;Only one password needed at boot&lt;/li&gt;
&lt;li&gt;Key file is protected by your main drive's encryption&lt;/li&gt;
&lt;li&gt;If someone steals just the secondary drive, they can't access it (no key file)&lt;/li&gt;
&lt;li&gt;If someone steals both drives but doesn't have your password, neither drive can be accessed&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Drive not mounting at boot
&lt;/h3&gt;

&lt;p&gt;Check if it's unlocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; /dev/mapper/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see &lt;code&gt;storage&lt;/code&gt; in the list.&lt;/p&gt;

&lt;p&gt;If not, check /etc/crypttab syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/crypttab | &lt;span class="nb"&gt;grep &lt;/span&gt;storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Permission denied when writing to drive
&lt;/h3&gt;

&lt;p&gt;Fix ownership:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; &amp;lt;your_username&amp;gt;:&amp;lt;your_username&amp;gt; /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Want to manually unlock/mount
&lt;/h3&gt;

&lt;p&gt;Unlock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup open /dev/nvme1n1p1 storage &lt;span class="nt"&gt;--key-file&lt;/span&gt; /root/keyfiles/storage.key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;mount /dev/mapper/storage /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Want to manually unmount/lock
&lt;/h3&gt;

&lt;p&gt;Unmount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;umount /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup close storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Forgot your LUKS passphrase
&lt;/h3&gt;

&lt;p&gt;If you still have access to the key file, you can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Unlock with the key file: &lt;code&gt;sudo cryptsetup open /dev/nvme1n1p1 storage --key-file /root/keyfiles/storage.key&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Change the passphrase: &lt;code&gt;sudo cryptsetup luksChangeKey /dev/nvme1n1p1&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Need to add another way to unlock (second key file or passphrase)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksAddKey /dev/nvme1n1p1 /path/to/new/keyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or to add a new passphrase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksAddKey /dev/nvme1n1p1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Useful Commands
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Check drive usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check encryption status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup status storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  View LUKS header information
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cryptsetup luksDump /dev/nvme1n1p1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  See all block devices
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsblk &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check btrfs filesystem info
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;btrfs filesystem show /home/&amp;lt;your_username&amp;gt;/storage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a btrfs snapshot (instant backup)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;btrfs subvolume snapshot /home/&amp;lt;your_username&amp;gt;/storage /home/&amp;lt;your_username&amp;gt;/storage/.snapshots/snapshot-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%Y%m%d&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;Now that your drive is set up, here are some great uses:&lt;/p&gt;

&lt;h3&gt;
  
  
  Storing Local LLMs
&lt;/h3&gt;

&lt;p&gt;Large language models can be 4GB to 70GB+ each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/llms
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/llms
&lt;span class="c"&gt;# Download your models here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Media Storage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/&lt;span class="o"&gt;{&lt;/span&gt;movies,music,photos&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Development Projects
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/projects
&lt;span class="nb"&gt;cd&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/projects
git clone https://github.com/yourproject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Backups
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/storage/backups
rsync &lt;span class="nt"&gt;-av&lt;/span&gt; /home/&amp;lt;your_username&amp;gt;/important-data /home/&amp;lt;your_username&amp;gt;/storage/backups/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Congratulations! You've successfully set up an encrypted secondary drive with automatic unlocking and mounting. Your Framework Desktop now has:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔒 &lt;strong&gt;Security:&lt;/strong&gt; Full disk encryption on both drives&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Convenience:&lt;/strong&gt; One password unlocks everything&lt;/li&gt;
&lt;li&gt;💾 &lt;strong&gt;Capacity:&lt;/strong&gt; 1.8TB of additional storage&lt;/li&gt;
&lt;li&gt;🎯 &lt;strong&gt;Modern:&lt;/strong&gt; Using btrfs with snapshots, compression, and integrity checking&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Store your LUKS passphrase safely, and enjoy your expanded storage!&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Document Information&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Created: 2025-11-09&lt;/li&gt;
&lt;li&gt;System: Omarch Linux (Arch-based)&lt;/li&gt;
&lt;li&gt;Hardware: Framework Desktop&lt;/li&gt;
&lt;li&gt;Drive: 2TB NVMe (/dev/nvme1n1)&lt;/li&gt;
&lt;li&gt;Mount Point: /home//storage&lt;/li&gt;
&lt;li&gt;Filesystem: btrfs&lt;/li&gt;
&lt;li&gt;Encryption: LUKS with key file auto-unlock&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Questions or Issues?&lt;/strong&gt;&lt;br&gt;
Refer to the Arch Wiki for detailed documentation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Dm-crypt/Encrypting_an_entire_system" rel="noopener noreferrer"&gt;dm-crypt/Encrypting an entire system&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Btrfs" rel="noopener noreferrer"&gt;btrfs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://wiki.archlinux.org/title/Fstab" rel="noopener noreferrer"&gt;fstab&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>arch</category>
      <category>omarchy</category>
    </item>
    <item>
      <title>Calculating Quarterly Taxes as a Freelancer</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Thu, 09 Jan 2025 18:01:10 +0000</pubDate>
      <link>https://forem.com/mrispoli24/calculating-quarterly-taxes-as-a-freelancer-27i0</link>
      <guid>https://forem.com/mrispoli24/calculating-quarterly-taxes-as-a-freelancer-27i0</guid>
      <description>&lt;p&gt;Calculating your taxes as a freelancer can be a daunting task. Often times we don't know exactly how much we will make come year end. If you overpay early in the year, it can leave you short on cash at the end, with no refund until the following year. What's a freelancer to do?&lt;/p&gt;

&lt;p&gt;This was one of the major reasons I built &lt;a href="https://www.finforecasting.com" rel="noopener noreferrer"&gt;fin&lt;/a&gt;. It helps you forecast your contracts against your expenses to see the reality of where you will end up.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Important note, this is does not constitute financial advice. The exact amount you will owe for taxation is based on many factors so ultimately you should review this with your accountant.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For those that don't want to read on, I made a video showcasing this below:&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/o4MAvMj5WaI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Let's say that I sign a 6 month contract here in January for $12,000.00 per month. When it comes time to compute my taxes for quarter one in April, my accountant typically assumes this to be a steady trend throughout the year. Therefore, I would compute my quarterly tax burden based on the net income of $36,000.00.&lt;/p&gt;

&lt;p&gt;I'll add this income to Fin and take a look at the cumulative vs. month over month recurring revenue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ksn0qv7io81wqdky77g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6ksn0qv7io81wqdky77g.png" alt=" " width="800" height="413"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The obvious first problem here is that I'm not taking into account any expenses, which is easy enough to remedy by tracking and deducting those from my total income.&lt;/p&gt;

&lt;p&gt;I'll add some expenses to my freelance practice and we can now see how this lays out for quarter one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqiekcylbger51df898y5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqiekcylbger51df898y5.png" alt=" " width="800" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With our expenses added we can say that we should plan to pay quarterly taxes on an income of $30,276.60. But what if we fail to secure another contract in July? &lt;/p&gt;

&lt;p&gt;Therin lies the problem with calculating our taxes this way. Using fin we can see how the next 12 months of our future looks if we fail to sign any new customers.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzh7l3lmhxab2v69274w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzh7l3lmhxab2v69274w.png" alt=" " width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3xy2pg1q9zi2i8xpjjd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3xy2pg1q9zi2i8xpjjd.png" alt=" " width="800" height="53"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We end the year with a profit of $49,106.40 far below our expectations when we cut a check for quarter one. This is the fear of every freelancer, we don't want to overpay our quarterly taxes and end up short on cash at the end of the year if we fail to secure another contract.&lt;/p&gt;

&lt;p&gt;In 2024 this was a reality for many as tech contracts dried up and work became harder to secure. With Fin, we can see further into the future and assess our risks accordingly.&lt;/p&gt;

&lt;p&gt;Now let's say we secure a contract for July through September but at a lower monthly rate of $8,000.00 per month. This changes our month over month projection to look more like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7a2b29opm1g5zrqnoccc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7a2b29opm1g5zrqnoccc.png" alt="fin month over month projection" width="800" height="483"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see a bit further into the future now but there's still a real risk we sign no contracts at the end of the year. Now we can see this and take a look at what our 12 month is instead of what it looks like at that point in time. &lt;/p&gt;

&lt;p&gt;You can also see that come October we're going to need to begin dipping into our harvested profits to cover our ongoing expenses.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vzo69nn3nullplzaj7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vzo69nn3nullplzaj7c.png" alt="fin month over month net profit" width="800" height="66"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ultimately, if we hold some cash back and use our 12-month net profit projection come June, we can cover the downside nicely should we hit a dry spell in terms of work. Otherwise we'd be forced to leverage credit cards and rely on a tax refund the following year to make up for the cash shortage.&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>career</category>
      <category>webdev</category>
      <category>money</category>
    </item>
    <item>
      <title>Kamal 2 Quick Start - the missing tutorial</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Wed, 08 Jan 2025 16:13:48 +0000</pubDate>
      <link>https://forem.com/mrispoli24/kamal-2-quick-start-the-missing-tutorial-4f5p</link>
      <guid>https://forem.com/mrispoli24/kamal-2-quick-start-the-missing-tutorial-4f5p</guid>
      <description>&lt;p&gt;Kamal is the new out of the box deployment tool that comes with Ruby on Rails. I'm assuming you already know this and are trying to get up and running quickly so let's skip the formality and get right into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Get Docker
&lt;/h2&gt;

&lt;p&gt;Yes, you do need to sign up for a Docker account first. Go to &lt;a href="https://www.docker.com" rel="noopener noreferrer"&gt;https://www.docker.com&lt;/a&gt; and sign up for an account. This will take you to your Docker Hub.&lt;/p&gt;

&lt;p&gt;From there you will want to select a &lt;em&gt;private repository&lt;/em&gt; for your project unless you want your build artifacts made public.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs71hsw2j7sjcaex7p764.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs71hsw2j7sjcaex7p764.png" alt=" " width="800" height="295"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Download Docker Desktop
&lt;/h2&gt;

&lt;p&gt;Now you should download docker desktop and install. You should then be able to run &lt;code&gt;docker login&lt;/code&gt; in your terminal and log in with your credentials. However, you don't want to necessarily log in with your password for you deploys. You'll want to use an access token instead...&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Create an Access Token
&lt;/h2&gt;

&lt;p&gt;Go to settings -&amp;gt; personal access tokens in Docker Hub and you can create a token there. Here's the link if you are already logged in: &lt;a href="https://app.docker.com/settings/personal-access-tokens" rel="noopener noreferrer"&gt;https://app.docker.com/settings/personal-access-tokens&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now try to run &lt;code&gt;docker login&lt;/code&gt; and enter the access token you just generated as a password. It should succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Save to .env File
&lt;/h2&gt;

&lt;p&gt;Now what you are going to want to do is create a .env file at the root of your Rails app. This is the part where AI will fail you and the existing documentation is sparse. &lt;strong&gt;Do not store this in your encrypted credentials&lt;/strong&gt; like your other secrets. Kamal specifically looks for this in a .env file. &lt;/p&gt;

&lt;p&gt;You should also not commit this to source, Rails by default will have this in the .gitignore file so you should be able to see it's not getting committed after creation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Update Your deploy.yml
&lt;/h2&gt;

&lt;p&gt;Open the &lt;code&gt;config/deploy.yml&lt;/code&gt; file. Here we can make some updates.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Update the &lt;code&gt;service&lt;/code&gt; value with the name of your app.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Update the &lt;code&gt;image&lt;/code&gt; value with your username slash the name of your app: &lt;code&gt;&amp;lt;your_docker_username&amp;gt;/&amp;lt;name_of_app&amp;gt;&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under &lt;code&gt;servers&lt;/code&gt; -&amp;gt; &lt;code&gt;web&lt;/code&gt; add the IP Address of your deploy target. If this is Digital Ocean like I used, this is the public IP of the server found in the admin panel.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under &lt;code&gt;proxy&lt;/code&gt; you will want the &lt;code&gt;ssl&lt;/code&gt; value to stay true if you want an ssl to be generated and the &lt;code&gt;host&lt;/code&gt; will be the URL of your app including subdomain if you are deploying to a subdomain. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Under &lt;code&gt;registry&lt;/code&gt; -&amp;gt; &lt;code&gt;username&lt;/code&gt; put your Docker username. Leave the &lt;code&gt;password&lt;/code&gt; value as is &lt;code&gt;- KAMAL_REGISTRY_PASSWORD&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Note: If you are just testing this and want to have it so you can hit the public IP address of your server in your browser without a domain name, first set the ssl value to false and set the host to the public IP address of the server for testing under &lt;code&gt;servers&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything else can remain as is. Here's what it should look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Name of your application. Used to uniquely configure containers.&lt;/span&gt;
&lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_APPLICATION_NAME&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Name of the container image.&lt;/span&gt;
&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;YOUR_APPLICATION_NAME&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Deploy to these servers.&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_SERVER_IP_ADDRESS&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;# job:&lt;/span&gt;
  &lt;span class="c1"&gt;#   hosts:&lt;/span&gt;
  &lt;span class="c1"&gt;#     - 192.168.0.1&lt;/span&gt;
  &lt;span class="c1"&gt;#   cmd: bin/jobs&lt;/span&gt;

&lt;span class="c1"&gt;# Enable SSL auto certification via Let's Encrypt (and allow for multiple apps on one server).&lt;/span&gt;
&lt;span class="c1"&gt;# Set ssl: false if using something like Cloudflare to terminate SSL (but keep host!).&lt;/span&gt;
&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_DOMAIN_NAME_INCLUDING_SUBDOMAIN_IF_APPLICABLE&amp;gt;&lt;/span&gt;

&lt;span class="c1"&gt;# Credentials for your image host.&lt;/span&gt;
&lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Specify the registry server, if you're not using Docker Hub&lt;/span&gt;
  &lt;span class="c1"&gt;# server: registry.digitalocean.com / ghcr.io / ...&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;YOUR_DOCKER_USERNAME&amp;gt;&lt;/span&gt;

  &lt;span class="c1"&gt;# Always use an access token rather than real password when possible.&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Deploy Time!
&lt;/h2&gt;

&lt;p&gt;Ok, now this last part is very important, it's time to deploy your app. First, make sure that you can ssh into the deploy target from your machine. I like to setup a firewall that only allows my IP Address to SSH into the machine so before you deploy make sure you can &lt;code&gt;ssh root@&amp;lt;YOUR_SERVER_IP_ADDRESS&amp;gt;&lt;/code&gt; before running deploy.&lt;/p&gt;

&lt;p&gt;If you have just set this server up you'll want to run &lt;code&gt;dotenv kamal setup&lt;/code&gt; first so that it installs docker and all dependencies on the remote server.&lt;/p&gt;

&lt;p&gt;Now you can run &lt;code&gt;dotenv kamal deploy&lt;/code&gt; and everything should run in your terminal and complete. &lt;/p&gt;

&lt;p&gt;That &lt;code&gt;dotenv&lt;/code&gt; piece deals with the fact that for some reason Kamal 2 cannot find the .env file by default. After much skimming of git issues and yelling at LLM's I arrived at that single glorious comment. Simply use &lt;code&gt;dotenv kamal deploy&lt;/code&gt; and all should work as expected.&lt;/p&gt;

&lt;p&gt;I hope this helps with your first Kamal 2 deploy.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>deploy</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Format on save for ERB and Ruby files in Zed IDE</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Tue, 15 Oct 2024 16:50:58 +0000</pubDate>
      <link>https://forem.com/mrispoli24/format-on-save-for-erb-and-ruby-files-in-zed-ide-568g</link>
      <guid>https://forem.com/mrispoli24/format-on-save-for-erb-and-ruby-files-in-zed-ide-568g</guid>
      <description>&lt;p&gt;Something that I really wanted when working with (Zed)[&lt;a href="https://zed.dev" rel="noopener noreferrer"&gt;https://zed.dev&lt;/a&gt;] and Ruby on Rails was format on save ability for my .rb and .erb files. The setup is simple so I thought I'd give it a share:&lt;/p&gt;

&lt;p&gt;First open your &lt;code&gt;settings.json&lt;/code&gt; file with &lt;code&gt;cmnd + ,&lt;/code&gt;  or going to Zed -&amp;gt; Settings -&amp;gt; Open Settings.&lt;/p&gt;

&lt;p&gt;Then add a key for languages with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...other_settings_here&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"languages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Ruby"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format_on_save"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"external"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./bin/bundle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"exec"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"rubocop"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"--auto-correct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"--stdin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"corrected.rb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"--stderr"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ERB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"formatter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"external"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="s2"&gt;"f=erblinttemp_$RANDOM$RANDOM.html.erb; cat &amp;gt; $f; erblint -a $f &amp;amp;&amp;gt;/dev/null; cat $f; rm $f;"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"external"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"htmlbeautifier"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"format_on_save"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"on"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There you have it format on save for your template files and ruby files ready to go.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>zed</category>
    </item>
    <item>
      <title>Kicking the tires with NestJS and Hotwire: Part II</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Wed, 15 Nov 2023 13:03:41 +0000</pubDate>
      <link>https://forem.com/mrispoli24/kicking-the-tires-with-nestjs-and-hotwire-part-ii-33fh</link>
      <guid>https://forem.com/mrispoli24/kicking-the-tires-with-nestjs-and-hotwire-part-ii-33fh</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/HGuA0pR-dc4"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I recently experimented with using &lt;a href="https://hotwired.dev/" rel="noopener noreferrer"&gt;Hotwire&lt;/a&gt;, the HTML-over-the-wire approach that comes default with the Ruby on Rails framework but with a node.js based backend. My goal was to try the paradigm out using a language I am familiar with, Typescript, to determine if this is a better developer experience and paradigm in general to build web applications with.&lt;/p&gt;

&lt;p&gt;If you haven't seen part one for the initial setup and tour, check it out here: &lt;a href="https://dev.to/mrispoli24/how-to-build-an-app-with-nestjs-and-hotwire-42j1"&gt;Part I: How to Build an App with Nestjs and Hotwire&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing Hotwire with NestJS
&lt;/h2&gt;

&lt;p&gt;If you want more details on the initial setup I encourage you to take a look at the &lt;a href="https://dev.to/mrispoli24/how-to-build-an-app-with-nestjs-and-hotwire-42j1"&gt;Part I&lt;/a&gt; that covers more of the initial implementation. For this portion, I added Prisma as an ORM, a frontend style library called Tachyons, and &lt;a href="https://alpinejs.dev/" rel="noopener noreferrer"&gt;AlpineJS&lt;/a&gt; to handle any client-side interactions. I did this to avoid needing to add a client-side bundler to the build and instead just rely on plain old module imports to compose the frontend. This is now the default for Rails and it is quite nice to not need any additional build tools for the client.&lt;/p&gt;

&lt;p&gt;I chose &lt;a href="https://tachyons.io/" rel="noopener noreferrer"&gt;Tachyons&lt;/a&gt; over Tailwind because Tachyons is an atomic CSS framework, similar to Tailwind, however it's much lighter weight. Tailwind tends to be a bit heavier without using post CSS processing so I wanted to stick with something smaller.&lt;/p&gt;

&lt;p&gt;I chose AlpineJS over the Hotwire default of Stimulus because Alpine does provide a lot of nice modules and features like modals and focus lock for accessibility that can be a pain to implement on your own. I think Alpine really has some great features that you will appreciate if you were going to build something more robust with this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Broadcasting Updates and Security Concerns
&lt;/h2&gt;

&lt;p&gt;A key benefit of Hotwire is streaming updates to multiple connected clients. However, this can also be a security risk. The vast majority of apps I've worked on do not require this type of functionality so broadcasting every user's updates to every connected client is a security risk in most cases.&lt;/p&gt;

&lt;p&gt;You could potentially avoid using turbo streams and just return the right HTML in the standard response of your API calls via turbo frames. The problem here is if an update requires you to update multiple components or frames on the page you would either need to specify the &lt;code&gt;target=_top&lt;/code&gt; property and refresh all the frames. However, this method could become heavy. Turbo streams allow us to target specific elements and update them by DOM selector.&lt;/p&gt;

&lt;p&gt;For example here we update the messages list as well as the messages count at the top of the page in a single turbo stream event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"append"&lt;/span&gt; &lt;span class="na"&gt;targets=&lt;/span&gt;&lt;span class="s"&gt;"#messages ul"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_{{message.id}}_list_item"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"bb pv3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_{{message.id}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        {{&amp;gt; message message=message}}
      &lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-stream&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;turbo-stream&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"update"&lt;/span&gt; &lt;span class="na"&gt;target=&lt;/span&gt;&lt;span class="s"&gt;"messages-count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"messages-count"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;strong&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"db mv3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Messages Count:
        {{messagesCount}}&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are really three core scenarios we need to cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updating only the user's client&lt;/li&gt;
&lt;li&gt;Updating everyone's client&lt;/li&gt;
&lt;li&gt;Updating only select authorized clients&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I came decided to implement a &lt;code&gt;broadcastTo&lt;/code&gt; flag that allowed for broadcasting updates to &lt;code&gt;self&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;, or &lt;code&gt;with-permissions&lt;/code&gt;. The last type would allow you to pass a predicate function to the controller where you could execute your authorization logic. As this is just a simple demo I left the &lt;code&gt;with-permissions&lt;/code&gt; and predicate function for a later day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventEmitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-stream.event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nl"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nl"&gt;requestStreamId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nl"&gt;broadcastTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;self&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;with-permissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="nl"&gt;predicate&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;broadcastTo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;self&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestStreamId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-turbo-stream-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestStreamId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-turbo-stream-id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
              &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt;

              &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;with-permissions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="c1"&gt;// TODO: Implement permissions predicate&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
              &lt;span class="c1"&gt;// This should never happen&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;never&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I implemented a cookie-based solution to only broadcast updates to the appropriate clients. This means that any client that connects the turbo stream endpoint gets assigned a unique identifier. We need this even in applications without log in so we can still send streams to the client that made the request of our other controllers. For example an ecommerce application would have guest users that add items to cart and we would want to update their cart at the top of the page.&lt;/p&gt;

&lt;p&gt;Here's some examples of calling the send event from a NestJS controller where we broadcast to all clients and just the requesting client or "self" respectively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendTurboStreamEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-stream.event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-streams/update-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;broadcastTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;all&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendTurboStreamEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-stream.event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-streams/create-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;broadcastTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;self&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;messagesCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That being said, I would be cautious using this method on anything production worthy. I put this together rather quickly and frameworks like Ruby on Rails, Laravel and Phoenix have been battle tested with this paradigm.&lt;/p&gt;

&lt;p&gt;So while possible with NestJS, you'd likely need to implement additional security yourself before using Hotwire streams in a real app. While the Turbo docs cover the client side setup, you are left to dig into the Ruby on Rails setup if you want to uncover how this is implemented in the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paradigm Shift from Declarative Frameworks
&lt;/h2&gt;

&lt;p&gt;Coming from React, I noticed Hotwire requires more imperative DOM updates. Instead of just mutating state and letting the UI update, you explicitly target DOM elements to update.&lt;/p&gt;

&lt;p&gt;This took some mental adjustment coming from React, however I can see the appeal as your updates are clean and straightforward to see based on action and targets set on the streams.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;turbo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;append&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#messages ul&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/turbo-stream&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;turbo&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;messages-count&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/turbo-stream&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also found going back to handlebars as a template language to be a bit lackluster. I do prefer JSX / TSX as a template language and feel if I were to take this further I would want to have a TSX to HTML implementation instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance and Scalability Unknowns
&lt;/h2&gt;

&lt;p&gt;I'm curious how the server sent events would perform at scale compared to modern SPAs. While server sent events are more efficient than WebSockets, it's hard to know how well this would handle hundreds or thousands of connections.&lt;/p&gt;

&lt;p&gt;I tend to not worry too much about scaling prematurely, however, you do want to have some idea of what scale could look like in terms of memory use and cost over time. In this case I'm not doing any benchmarking, but it would be something I would want to test before rolling out in production.&lt;/p&gt;

&lt;p&gt;My gut feeling is that if you really love this paradigm there is one language and framework that stands out as designed precisely for this type of use case. Phoenix LiveView leveraging Elixir and running on top of BEAM, the ER Lang virtual machine, is the perfect tool for this job. I could also see GoLang being a nice contender for handling all of the connections, but of course you would have to build up that implementation yourself first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better Options for Production Apps
&lt;/h2&gt;

&lt;p&gt;For me, NestJS with Hotwire seems like a fun experiment but not ideal for serious production apps yet. Modern meta-frameworks like NextJS, Nuxt, SvelteKit and others offer a better developer experience and confidence in scalability for the JavaScript ecosystem. &lt;/p&gt;

&lt;p&gt;However, for developers using other languages, Hotwire is an incredible way to write more code in your favorite language and avoid the cognitive overhead of toggling between the frontend and backend in two different languages. If your language of choice doesn't have it's own HTML-over-the-wire style framework like Elixir, Ruby, and PHP--you should give Turbo a look for implementing this paradigm. &lt;/p&gt;

&lt;p&gt;That being said, if you love this paradigm and have to learn a new language anyway to take advantage of this I feel Phoenix LiveView is probably the best place to put your efforts. It handles streaming and security out of the box and leverages Elixir/Erlang and the BEAM virtual machine for rock-solid scalability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hotwire with NestJS works well but the developer experience is likely not going to push you away from the popular meta frameworks like NextJS, Nuxt, or SvelteKit.&lt;/li&gt;
&lt;li&gt;The paradigm shift from declarative frameworks like React takes some adjustment.
&lt;/li&gt;
&lt;li&gt;For production use, I definitely recommend using it with Ruby on Rails since the community has already handled most of the backend concerns.&lt;/li&gt;
&lt;li&gt;You definitely don't need to implement a client side bundler if you don't want to anymore.&lt;/li&gt;
&lt;li&gt;Phoenix LiveView feels like the right tool for this job if you had to learn something new.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let me know if you have any other questions! I'm happy to discuss more.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>rails</category>
    </item>
    <item>
      <title>How to build an app with nestjs and hotwire!</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Thu, 02 Nov 2023 18:38:47 +0000</pubDate>
      <link>https://forem.com/mrispoli24/how-to-build-an-app-with-nestjs-and-hotwire-42j1</link>
      <guid>https://forem.com/mrispoli24/how-to-build-an-app-with-nestjs-and-hotwire-42j1</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/LCWXJlboouU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;TLDR; If you just want to fire up the code and try it out, here's the git repository: &lt;a href="https://github.com/Cause-of-a-Kind/nest-hotwire-boilerplate" rel="noopener noreferrer"&gt;https://github.com/Cause-of-a-Kind/nest-hotwire-boilerplate&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;A few years ago, 37 Signals announced Hotwire, a new approach to building web applications without writing front-end JavaScript. Hotwire was developed by the team at 37 Signals while building Hey.com and allows you to have the responsiveness of a single-page app using only back-end code. &lt;/p&gt;

&lt;p&gt;I've been intrigued by similar frameworks like Phoenix LiveView and Laravel Livewire, but as a JavaScript developer, I didn't want to have to learn a whole new backend language to try them out. While I had a bunch of Ruby on Rails experience a number of years ago, my knowledge of the framework has grown rusty.&lt;/p&gt;

&lt;p&gt;Hotwire is backend agnostic, so I decided to test it out using NestJS, a Node.js framework that has an MVC structure similar to Rails. The idea is I could kick the tires on this new paradigm without my opinion being clouded by my unfamiliarity with a different backend language.&lt;/p&gt;

&lt;p&gt;Hotwire is composed of three main libraries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turbo&lt;/li&gt;
&lt;li&gt;Stimulus&lt;/li&gt;
&lt;li&gt;Strada&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I focused on is Turbo for this demo. This is the library that powers the HTML over the wire (H-OT-WIRE) approach to building your application. Stimulus is specifically for manage your client side javascript, and isn't a required part of this. You can use plain Vanilla JS or you can use a different client-side library like Alpine JS as an alternative.&lt;/p&gt;

&lt;p&gt;In the demo I go over the three main components of Turbo for web apps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Turbo Drive&lt;/li&gt;
&lt;li&gt;Turbo Frames&lt;/li&gt;
&lt;li&gt;Turbo Streams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Drive
&lt;/h2&gt;

&lt;p&gt;The first part of Hotwire I tried was Turbo Drive. This eliminates full page reloads when navigating between pages. To enable it, I simply added the Hotwire package script from Skypack to the document &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and voila.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
...
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hotwiredTurbo&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.skypack.dev/@hotwired/turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
...
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With Turbo Drive, clicking between pages fetched the new page in the background and replaced the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; contents instead of fully reloading.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Frames
&lt;/h2&gt;

&lt;p&gt;Next, I added Turbo Frames, which are effectively the &lt;em&gt;components&lt;/em&gt; of your application that can update independently of each other. &lt;/p&gt;

&lt;p&gt;For example, I added a &lt;code&gt;&amp;lt;turbo-frame&amp;gt;&lt;/code&gt; that loads a list of messages from the path in its &lt;code&gt;src&lt;/code&gt; attribute. The contents of this initial frame serve as the loading state before that html is replaced by the server response from &lt;code&gt;/messages&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/messages"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Loading...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I then created an endpoint that returns those messages as a fragment of HTML. You can serve this as a complete HTML document or just a piece of HTML like I have and Turbo will pluck that fragment from the response and swap it with the existing HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-frames/messages&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;getMessages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMessages&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the &lt;code&gt;turbo-frames/messages&lt;/code&gt; fragment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"messages"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Messages&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {{#each messages}}
      &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_{{this.id}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{{message.text}}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/messages/{{message.id}}/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
    {{/each}}
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the result:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdn5paau6r9plskzeqlgq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdn5paau6r9plskzeqlgq.png" alt="our messages list rendered" width="341" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When clicking edit on a message, it loads the edit form into the frame without reloading the whole page. And this is really the paradigm shift you have to wrap your mind around if you're coming from another framework like React. Instead of managing the state of the UI on the client, we simply use standard link tags and Turbo loads the returned frame in place of what's there. &lt;/p&gt;

&lt;p&gt;If you take a look at the code above again, we have another &lt;code&gt;turbo-frame&lt;/code&gt; element inside of our messages frame that holds each message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;{{#each messages}}
  &lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_{{this.id}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{{message.text}}&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/messages/{{message.id}}/edit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Edit&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
{{/each}}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we click on the link, we create another endpoint that returns the new piece of UI we want to load in place of what is within that frame. &lt;/p&gt;

&lt;p&gt;Here is our controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/messages/:id/edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-frames/edit-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;editMessage&lt;/span&gt;&lt;span class="p"&gt;(@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is the &lt;code&gt;turbo-frames/edit-message&lt;/code&gt; template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;turbo-frame&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"message_{{message.id}}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/messages/{{message.id}}/edit"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;
           Message:
           &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{message.text}}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
       &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Update Message&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/turbo-frame&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which renders like this when we click edit:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa0j1vejb14o81ma0mi9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faa0j1vejb14o81ma0mi9.png" alt="rendering the edit form" width="474" height="289"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can submit the form just like a normal form without any client side js on our part. Our &lt;code&gt;/messages/:id/edit&lt;/code&gt; endpoint can return the HTML fragment it should replace the form with. In this world the server just returns the new state of the world for our frames and Turbo will replace what's there with the response.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/messages/:id/edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-frames/view-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, after submitting the form successfully, it switches back to our standard message view. Turbo Frames allow you to break the page into components that update independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxojf9i50el61fbz3hcw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faxojf9i50el61fbz3hcw.png" alt="updating ui after successful form submission" width="455" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Turbo Streams
&lt;/h2&gt;

&lt;p&gt;The most exciting part was getting real-time updates working with Turbo Streams. This uses Server-Sent Events to push data updates from the server to the client.&lt;/p&gt;

&lt;p&gt;I connected a stream source in my layout and set up an event emitter in NestJS. Now when a message is edited, it automatically streams the update to all connected clients.&lt;/p&gt;

&lt;p&gt;So returning to our script in the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of our document we can add our connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;hotwiredTurbo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;connectStreamSource&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.skypack.dev/@hotwired/turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;es&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/turbo-streams&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;connectStreamSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;es&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we have to enable the &lt;code&gt;/turbo-streams&lt;/code&gt; endpoint to allow for our client to connect to the server and receive events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Sse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/turbo-streams&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;sse&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fromEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventEmitter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-stream.event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;MessageEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, we're going to use and event emitter so that our backend can emit an event called &lt;code&gt;turbo-stream.event&lt;/code&gt; and deliver the template as a payload to the client.&lt;/p&gt;

&lt;p&gt;So now we can modify our edit messages controller to emit an event that will tell anyone else connected to the app to update their UI in real time. &lt;/p&gt;

&lt;p&gt;This enables real-time features like chat without any of our own front-end code!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/messages/:id/edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-frames/view-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;updateMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Param&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;Req&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;editMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendTurboStreamEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-streams/update-message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I created a small helper in our app service to handle rendering the template string and emitting the event when complete.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sendTurboStreamEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventEmitter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;turbo-stream.event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when if we load our application side by side with another instance you can see them both update automatically! This reminded me a lot of the magic that was baked into the Meteor framework, but with a lot less code. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvtzze3gah2uhvs0gafo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpvtzze3gah2uhvs0gafo.png" alt="updating our app side by side" width="707" height="298"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;I was impressed by how much I could build with Hotwire and NestJS. It seems promising for rapid prototyping and MVPs. I'm interested to try migrating an existing app that is using React to compare the difference. But now I can get a more accurate comparison being in the comfort of my daily programming language.&lt;/p&gt;

&lt;p&gt;I've published the demo app code to GitHub if you want to try playing with Hotwire yourself. Let me know if you build something cool with it!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Cause-of-a-Kind/nest-hotwire-boilerplate" rel="noopener noreferrer"&gt;https://github.com/Cause-of-a-Kind/nest-hotwire-boilerplate&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Is there a basic explanation of Web3 somewhere for my mom?</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Tue, 30 Aug 2022 14:49:59 +0000</pubDate>
      <link>https://forem.com/mrispoli24/is-there-a-basic-explanation-of-web3-somewhere-for-my-mom-4in6</link>
      <guid>https://forem.com/mrispoli24/is-there-a-basic-explanation-of-web3-somewhere-for-my-mom-4in6</guid>
      <description>&lt;p&gt;There isn't, but there should be! 💡&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbba3jgr80p51xsqjujac.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbba3jgr80p51xsqjujac.png" alt="web3 explained in a tweet" width="556" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Blockchain is a tool for building Web3, which to me simply means the internet of value (owning digital things). But that also means blockchain need not be a part of that equation at all.&lt;/p&gt;

&lt;p&gt;To take advantage of it, your Mom shouldn't need to understand the inner mechanics. I have a refrigerator, what does it do? It keeps food cold. You needn't know the inner workings to take advantage of it's awesome power.&lt;/p&gt;

&lt;p&gt;We all pay for goods with money, yet only a select few understand the economics behind fiat. Web3 needs to bridge that gap.&lt;/p&gt;

&lt;p&gt;This might rub some purists the wrong way though. It means the average person cares very little about decentralization. What they will care about is being able to log in to applications without having to remember a password or transfer money from place to place without waiting for a clearing house or being charged a transaction fee.&lt;/p&gt;

&lt;p&gt;God forbid we allow financial transactions to take place instantly, without paying a middleman... 🤯&lt;/p&gt;

&lt;p&gt;I find Web3 seems to conflate the technology behind it with what makes it valuable to a consumer.&lt;/p&gt;

&lt;p&gt;But would people be skeptical of HOW it allowed them to log in without a password like they are used to?&lt;/p&gt;

&lt;p&gt;Does anyone know if those GDPR cookie popups actually work on every website? Do they ask how our passwords are stored?&lt;/p&gt;

&lt;p&gt;I've met developers with years of experience that have no idea how traditional password-based authentication works. I'd imagine most people know even less than that with the tech we have right now.&lt;/p&gt;

&lt;p&gt;Someone once told me the little red emergency brakes 🚨 on the train are actually connected to nothing. They're just there to make you feel good. I think of this metaphor a lot when browsing the web. So many popups, certifications, and legalese. Yet there are so many data leaks, breaches, and vulnerabilities on trusted platforms.&lt;/p&gt;

&lt;p&gt;What I think is cool about Web3 is the idea that it will enable great security. How it achieves that will grow less and less important to consumers as it becomes mainstream.&lt;/p&gt;

</description>
      <category>watercooler</category>
      <category>discuss</category>
      <category>web3</category>
      <category>learning</category>
    </item>
    <item>
      <title>The Secret UX of the Headless CMS</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Tue, 30 Aug 2022 13:48:41 +0000</pubDate>
      <link>https://forem.com/mrispoli24/the-secret-ux-of-the-headless-cms-1nmh</link>
      <guid>https://forem.com/mrispoli24/the-secret-ux-of-the-headless-cms-1nmh</guid>
      <description>&lt;p&gt;With headless content management systems, designers and developers are given incredible power and flexibility. Power we never had with the monolithic solutions of the past like Wordpress, Drupal, Shopify, or Magento. The problem is that with great power and flexibility, comes great complexity. We spend a lot of time and energy on the design of our interfaces for customers. Yet, we think very little about the user experience of the back end systems that go into this. &lt;/p&gt;

&lt;p&gt;The prevailing belief is that employees and administrators can suffer the pain as long as the user experience for customers is great. When it comes to the classical content management systems, many of us have been institutionalized. We're years away from the pain of learning our way around Wordpress or Drupal or BigCommerce. When we build for headless, a CMS like &lt;a href="https://www.contentful.com/" rel="noopener noreferrer"&gt;Contentful&lt;/a&gt; or &lt;a href="https://www.contentstack.com/" rel="noopener noreferrer"&gt;Contentstack&lt;/a&gt; can be a dream. However, because the configuration is largely dependent on the developer setting it up, the user experience for your internal teams can manifest as a nightmare. &lt;/p&gt;

&lt;p&gt;This is the side of headless that the industry itself still has not come around to. There is an entire user experience that now must be crafted behind the CMS itself. In fact, headless content management systems can be used to house far more than just content. At Cause of a Kind, we're using headless systems to build entire command and control centers for applications. They come complete with the ability to modify the content and imagery, craft event triggers with web hooks, and even modify the UI of the CMS itself. They give us everything we need not just to control our web interfaces, but also a way to tie together disparate internal systems. They help us wrangle a tangled ball of third-party systems into a neat pipeline of data. We can then safely give control of these systems to non-technical stakeholders. &lt;/p&gt;

&lt;p&gt;This frees development teams from one line text changes and running simple scripts to migrate data from one system to another. We've used headless content management systems to build everything from complex brochure and e-commerce websites, to feature flag systems for continuous integration, all the way to medical software that aggregates data from remote devices. This type of power gives us the ability to deliver value faster to customers and more power to internal stakeholders. That being said, leaving this kind of UX design to a team of engineers is often the place where things go south.&lt;/p&gt;

&lt;p&gt;Headless content management systems have created their own &lt;a href="https://en.wikipedia.org/wiki/Uncanny_valley" rel="noopener noreferrer"&gt;uncanny valley&lt;/a&gt;. The closer the system gets to a great user experience, the more intolerable things can become for users. For example, let's take an old school CMS like Joomla. When modified it becomes quite odd to use. It is riddled with terms like &lt;em&gt;articles&lt;/em&gt;, &lt;em&gt;modules&lt;/em&gt;, and &lt;em&gt;components&lt;/em&gt; that end up representing different entities than we would expect. However, it is understood by the users that there will be a learning curve. They will have to memorize where these things go and what they will effect. The expectation is that this system &lt;em&gt;WILL NOT&lt;/em&gt; be intuitive, and this is understood as soon as the user logs in. &lt;/p&gt;

&lt;p&gt;With legacy content management systems, you didn't have to consider the user experience. The user experience was dictated for you. You suffered repurposing entities like &lt;em&gt;posts&lt;/em&gt;, &lt;em&gt;blocks&lt;/em&gt;, &lt;em&gt;components&lt;/em&gt;, and &lt;em&gt;articles&lt;/em&gt; for new use cases. Administrators decorated their monitors with post-it notes full of little tips for how to use the CMS. Internal stakeholders understood the tradeoff of using what's available to avoiding building something from scratch. These oddities became acceptable abstraction leaks and a challenging user experience was the expectation. &lt;/p&gt;

&lt;p&gt;Contrast this with a headless content management system. Here, the workflows can be carefully crafted, entity terminology can matched to the industry, and custom plugins can make the editing experience more intuitive. The problem is, when users expect something intuitive and simple to use, yet it is not, that friction if felt ten fold! It's even worse when you get &lt;em&gt;almost&lt;/em&gt; everything right. &lt;/p&gt;

&lt;p&gt;So how do we get this right? You need to talk to the users! It's important during the process of configuring the CMS, that you, the engineer, sit down and talk with some users. Show them how you're thinking of configuring things. Ask them about their workflows and pain points. At &lt;a href="https://www.causeofakind.com" rel="noopener noreferrer"&gt;Cause of a Kind&lt;/a&gt; the user flows in the back of the house make it to the story map and design documents. &lt;/p&gt;

&lt;p&gt;We may not need UX designers to create mockups like we do for the customer facing application, but we certainly need to prototype. Before you hook up the CMS, create some models and fill in some data. Share it with the people that manage the system and ask for their feedback. You won't get things perfect, but you'll be able to cross that uncanny valley between expected discomfort and that thing that was &lt;em&gt;almost&lt;/em&gt; delightful. &lt;/p&gt;




&lt;p&gt;Originally published on the Cause of a Kind blog: &lt;a href="https://www.causeofakind.com/the-salon/the-secret-ux-of-the-headless-cms" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>design</category>
      <category>devjournal</category>
      <category>architecture</category>
      <category>business</category>
    </item>
    <item>
      <title>On Building Bad Ideas</title>
      <dc:creator>Mike Rispoli</dc:creator>
      <pubDate>Mon, 22 Aug 2022 15:58:03 +0000</pubDate>
      <link>https://forem.com/mrispoli24/on-building-bad-ideas-14a2</link>
      <guid>https://forem.com/mrispoli24/on-building-bad-ideas-14a2</guid>
      <description>&lt;p&gt;As an agency owner, I'm the guy that has to pretend to like your bad ideas. I've heard all ranges of bad ideas. I've listened to a guy trying to sell jarred air from different parts of the world. I've entertained building a play to earn game described as "a total Ponzi Scheme." Some days I hear so many bad ideas I want to hang it all up and close my laptop for good. I'll move the whole family into a Yurt in the woods--never to look at another glowing screen again.&lt;/p&gt;

&lt;p&gt;When you own an agency, especially one that builds software, you're going to hear a lot of dreadful ideas. Since you get paid to build things, it behooves you to pretend to like any idea someone is willing to throw money at. You listen, nod, repeat the idea back and why it's good. You then proceed to entertain what it would take to build said idea.&lt;/p&gt;

&lt;p&gt;In situations like this, I often return to a quote by David Ogilvy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I never assign a product to a writer unless I know that he is  personally interested in it. Every time I have written a bad campaign,  it has been because the product did not interest me."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He's not saying that the writer must be a user of the product, but the writer should be at least interested in it. This same idea applies to building software. It's going to be difficult to build software that you don't see utility in. While you may not be a user of the software, you should at least believe in its necessity for someone. Building a product without empathy spells doom before you've initialized the repository.&lt;/p&gt;

&lt;p&gt;Most of the time, this feeling is subtle. It's rare to hear an outright bad idea. Most of the time they are ideas we don't understand or can't empathize with the problem. These are the ideas that are harder to walk away from. The it's not you it's me of agency pitches. You hate to walk away because you think there &lt;em&gt;might&lt;/em&gt; be value in this thing, if only you can understand it. &lt;/p&gt;

&lt;p&gt;In these situations it's easy to fall victim to someone's enthusiasm. What you need to ask yourself in these situations is: &lt;/p&gt;

&lt;p&gt;How successful is this person going to be at convincing customers to use their solution? Even a great idea has a large chasm to cross when it comes to converting existing behavior. If it solves a pain point, is the existing pain painful enough? Is this a 10x improvement over what exists? Remember, most software doesn't fail because it's poorly built. It fails because it purported to solve a problem that wasn't big enough.&lt;/p&gt;

&lt;p&gt;I was once given the advice when hiring, "it's OK to walk away from good people, but it's not OK to hire bad people." When in doubt about a candidate, it's better to walk away than be unsure. I now feel the same way about client ideas.  It's OK to walk away from a good idea, but it's not OK to build software on a premise you do not understand.&lt;/p&gt;

&lt;p&gt;So if you have an idea that you want an honest opinion on, give me a call. I'll always lend an open ear but I won't give you a price unless I like what you're building.&lt;/p&gt;




&lt;p&gt;Originally posted on the Cause of a Kind blog: &lt;a href="https://www.causeofakind.com/the-salon/on-building-bad-ideas" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

</description>
      <category>leadership</category>
      <category>management</category>
      <category>business</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
