<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mileswoodroffe.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mileswoodroffe.com/" rel="alternate" type="text/html" /><updated>2026-02-04T21:57:12+00:00</updated><id>https://mileswoodroffe.com/feed.xml</id><title type="html">Miles Woodroffe</title><subtitle>I help entrepreneurs, startups and scaleups build and deliver modern software products, services and teams.</subtitle><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><entry><title type="html">Ruby on Rails + Claude Code = Magic</title><link href="https://mileswoodroffe.com/articles/claude-on-rails" rel="alternate" type="text/html" title="Ruby on Rails + Claude Code = Magic" /><published>2026-02-04T00:00:00+00:00</published><updated>2026-02-04T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/claude-rails-comes-of-age</id><content type="html" xml:base="https://mileswoodroffe.com/articles/claude-on-rails"><![CDATA[<h2 id="8-months-ago">8 Months Ago</h2>

<p>It’s now 8 months since I wrote about an <a href="/articles/claude-rails-and-me">inflection point with Claude, Rails and me</a>. In that
article I wrote:</p>

<blockquote>
  <p>Claude Code with Rails feels like magic. I’ve avoided and actively ignored the hype, I’m a 
professional sceptic, but the last week deep dive has convinced me:</p>
</blockquote>

<blockquote>
  <p>“For experienced Rails developers, there’s orders of magnitude of productivity gains to be had. Right now. No joke.”</p>
</blockquote>

<blockquote>
  <p>It may not be the 10x developer we hear so much about, but without 
question, 2x to 3x is what I’m experiencing. If we can scale that over teams, it’s transformational.</p>
</blockquote>

<h2 id="fast-forward-to-today">Fast Forward to Today</h2>

<p>Since then I leaned in to using Claude on personal projects and became more and more comfortable with 
the flow, branch, plan, iterate, review. The first sign that things were accelerating rapidly was when 
I was hitting timeouts several times a day as I’d hit the limits of the $20 plan. Initially it was a 
good way to take a break, or go back to coding myself, but it wasn’t long before I upgraded to Max.</p>

<p>Since then my usage and success with Claude Code has been nothing short of exponential. It’s not an 
exaggeration to say my previous 2x or 3x boost really is in that mythical 10x range.</p>

<p><img src="/assets/images/articles/insights.webp" alt="yikes" /></p>

<p>This acceleration has crept up on me, but I can really say in the last couple of months, since December 
2025 at least, Claude seems to have hit another inflection point. Where previously I was reworking things
almost all the time, fixing, correcting, that slowly stopped happening to the point now, with carefully 
curated AGENTS.md and SKILL.md files, Claude is usually on target first time.</p>

<p>Certainly Opus 4.5 has a lot to do with this. But the Claude client has also come on leaps and bounds - those 
small things you barely notice but make every interaction more polished.</p>

<h2 id="the-word-is-out">The Word is Out</h2>

<p>Yesterday I woke up to a bunch of messages from friends linking to a <a href="https://x.com/i/status/2018368128108167344">tweet from Garry Tan</a> where he said:</p>

<blockquote>
  <p>I think people are sleeping a bit on how much Ruby on Rails + Claude Code is a crazy unlock - I mean Rails was designed for people who love syntactic sugar, and LLMs are sugar fiends.</p>
</blockquote>

<p><img src="/assets/images/articles/twitter.webp" alt="@garrytan - twitter" /></p>

<p>Took me a while to notice my name was in his screenshot - hence the DMs! Ironically I think that screenshot is from 
Claude itself, as that’s how attribution links look there, so ‘mileswoodroffe’ means it was attributing that 
quote to this very blog! Claude quoting me talking about Claude?!?</p>

<p>The key point I was making, as is Garry and many others, is Claude feels particularly suited to Ruby on Rails, and 
extremely token efficient, due to the well known conventions at its core. Even better, I also think it works in the 
other direction. When we review the code Claude delivers, due to Ruby’s incredibly clear syntax we can easily and quickly understand the output and know what to do next. When you compound this over a session of a couple of hours, this is a massive boost to flow.</p>

<h2 id="examples">Examples</h2>

<p>For example, I’d not worked on my <a href="https://apps.apple.com/us/app/1500cals/id6470238113">meal planning app 1500cals</a> 
for close to a year. I use it every day to plan meals, snacks and track exercise. I love it but didn’t have a lot of time or motivation to improve. There were a couple of features I’d long wanted but never quite got to. Time for Claude to help.</p>

<p>The first was around tracking points for exercise. I use Vitality and the points really push me to keep 
active so I wanted something similar. The 1500cals iOS app already syncs Apple Health steps and workout data so the 
foundations were there. I literally took a screenshot of the two key screens on Vitality, dragged them 
in to the Claude Code console, and then explained what they showed and how I wanted to integrate into 
my app.</p>

<p>Within seconds Claude had parsed this information, and set about with the <code class="language-plaintext highlighter-rouge">erb</code> templates. The first result
was staggeringly good. What surprises me are the details. Just from this screenshot it could interpret that 
the circles represented completion of 3, 5 or 8 steps, that I wanted to paginate monthly, it added a summary 
underneath that I hadn’t described or frankly even considered.</p>

<p><img src="/assets/images/articles/this-week.webp" alt="Daily Points" /></p>

<p>Further, it had proposed database change to persist this data, implemented models and controllers - even 
added and scheduled a job to update the data each night as Claude knew I was using Solid Queue. And it 
just worked. I reviewed the code, made a couple of tweaks and had to iterate over some additions but this
feature was done and deployed to production in under 30 minutes. If I had to estimate the time to build 
myself I would say at least a few hours. That’s being optimistic, and the reality also is I’d mulled this 
over for 6 months or more but hadn’t managed to get started. With the acceleration I get from Claude this 
opens up much more.</p>

<h2 id="disposable-code">Disposable Code</h2>

<p>When ideas can be brought to life at this pace it ushers in a new way of working. I always start a 
branch, work up an idea and if its not gelling, I just delete the branch and move on. The innovation is and 
always has been the ideas, the code is just how we present these ideas to users and make them interactive. 
Previously we laboured over our code, but now we can implement these ideas so effortlessly and cheaply, 
the code essentially becomes a disposable commodity.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>8 months ago in the previous Claude article I said I was all in. I have been ever since. Viva Rails. Viva Claude.</p>

<p>Would love to hear your experiences with LLM’s so don’t hesitate to drop me a note at my new favourite 
place <a href="https://bsky.app/profile/mileswoodroffe.com">Blue Sky</a> or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><category term="ai" /><summary type="html"><![CDATA[8 Months Ago]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/twitter.webp" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/twitter.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">An Out of Body Experience with Turbo</title><link href="https://mileswoodroffe.com/articles/out-of-body-experience-with-turbo" rel="alternate" type="text/html" title="An Out of Body Experience with Turbo" /><published>2025-09-07T00:00:00+00:00</published><updated>2025-09-07T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/out-of-body-experiences-with-turbo</id><content type="html" xml:base="https://mileswoodroffe.com/articles/out-of-body-experience-with-turbo"><![CDATA[<h2 id="messengers">Messengers</h2>

<p>I needed to add a third-party messenger widget to a project I’m working on, and given their widespread 
use on many sites, I figured this would be a trivial case of dropping some JavaScript into the page 
and we’d be done. In fact, it was that simple! Until I navigated off the first page, which is when 
the chat bubble disappeared, and this story begins! What’s going on?</p>

<h2 id="rails-turbo-drive">Rails Turbo Drive</h2>

<p>For a bit of context, this is of course a modern Rails web site, with all the amazing out-of-the-box 
benefits we all know and love. On the front end, Turbo is one of these incredible features, and the 
default Turbo Drive is so transparent it can be easy to forget it’s even there.</p>

<p>With Turbo Drive, unlike a traditional page load that would replace the entire HTML, turbo intercepts 
link clicks and form submissions, fetches the full HTML with AJAX, and then replaces just the &lt;body&gt; element 
with the new content. This results in much faster rendering when navigating since all the head content 
like JavaScript, CSS etc doesn’t need to be re-parsed. Genius!</p>

<h2 id="the-problem">The Problem</h2>

<p>This approach works beautifully for most content, but it creates a problem for third-party widgets that 
inject themselves into the DOM. When the messenger widget’s JavaScript runs on the initial page load, 
it inserts the code necessary to render and manage the chat window directly in the document body.
As soon as we navigate to another page, Turbo Drive throws away that entire body content—including 
my carefully positioned chat widget—and replaces it with the fresh HTML that has no knowledge of the 
messenger integration since the initialiser code does not run.</p>

<video controls="" width="100%">
  <source src="/assets/videos/messenger-1.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h2 id="permanent">Permanent</h2>

<p>Of course, Rails has <code class="language-plaintext highlighter-rouge">data-turbo-permanent</code> for this kind of situation right? From the <a href="https://turbo.hotwired.dev/handbook/building#persisting-elements-across-page-loads">documentation</a>:</p>

<blockquote>
  <p>Turbo Drive allows you to mark certain elements as permanent. Permanent elements persist across 
page loads, so that any changes you make to those elements do not need to be reapplied after navigation.</p>
</blockquote>

<p>This sounds like the solution right? It’s a little tricky because the dixa-messenger markup is dynamically 
loaded after the page is rendered so we can’t just add <code class="language-plaintext highlighter-rouge">data-turbo-permanent</code> easily. To get around this 
I added a new <code class="language-plaintext highlighter-rouge">&lt;div id="dixa-permanent-container" data-turbo-permanent&gt;&lt;/div&gt;</code> to our layout and then 
adapted the JavaScript adding a <code class="language-plaintext highlighter-rouge">MutationObserver</code> that watched the page and as soon as the dixa widget 
had been added asynchronously, moves it to our data-turbo-permanent div. Clever huh? Well.. no</p>

<video controls="" width="100%">
  <source src="/assets/videos/messenger-2.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h2 id="iframes">iFrames</h2>

<p>Since the widget is implemented with an iFrame, as far as I can ascertain, iFrames lose all their state and
crucially the connection to the third party remote widget platform. So despite our efforts the widget still does not survive
across Turbo Drive navigations. We can see the chat bubble circle and ghostly image of where the chat window 
previously was, but the content rendered from the remote service is lost.</p>

<h2 id="out-of-body-experience">Out of Body Experience</h2>

<p>Now I understood the issue was Turbo replacing the body, and that due to being an iFrame that turbo-permanent would 
not resolve the issue, I had an idea to move the widget completely outside of the body tag. 
This seemed wrong but worth a try. Bingo! Using the same trick to observe the page and then move the div, but this 
time move it outside the body of the page worked perfectly. Our widget icon persists across navigations, and even 
with the chat window open, state is maintained!</p>

<video controls="" width="100%">
  <source src="/assets/videos/messenger-3.mp4" type="video/mp4" />
  Your browser does not support the video tag.
</video>

<h2 id="rails-world-2025">Rails World 2025</h2>

<p>It just so happens that Rails World conference was last week, so I took this opportunity to ask some of the best and 
brightest in the business. Great and helpful conversations with many, but DHH himself pointed me to 
<a href="https://github.com/hotwired/turbo/pull/305">this issue</a> which describes the same problem and suggests intercepting 
the body replacement with <code class="language-plaintext highlighter-rouge">turbo:before-render</code> and then using an inner div that Turbo Drive replaces on each page 
render. That means our widget is not replaced and functions as we expect.
It’s a bunch more code though, and despite not being technically ‘legal’ HTML markup, the out of body fix 
described previously is still my preference.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>If you have any ideas, opinions, or better suggestions, don’t hesitate to let me know. I spent way longer than I should have 
trying to work this out so sharing this post in case anyone else out there finds it of any use. 
As always, find me on <a href="https://bsky.app/profile/mileswoodroffe.com">Blue Sky</a> or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Messengers]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/dixa-messenger.jpg" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/dixa-messenger.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Claude, Rails and me - An Inflection Point</title><link href="https://mileswoodroffe.com/articles/claude-rails-and-me" rel="alternate" type="text/html" title="Claude, Rails and me - An Inflection Point" /><published>2025-06-14T00:00:00+00:00</published><updated>2025-06-14T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/claude-rails-and-me</id><content type="html" xml:base="https://mileswoodroffe.com/articles/claude-rails-and-me"><![CDATA[<h2 id="tldr">TL;DR</h2>

<p>Claude Code with Rails feels like magic. I’ve avoided and actively ignored the hype, I’m a 
professional sceptic, but the last week deep dive has convinced me:</p>

<blockquote>
  <p>For experienced Rails developers, there’s orders of magnitude of productivity gains to be had. Right now. No joke.</p>
</blockquote>

<p>It may not be the 10x developer we hear so much about, but without 
question, 2x to 3x is what I’m experiencing. If we can scale that over teams, it’s transformational. 
Read on…</p>

<h2 id="the-rise-of-the-transformer">The Rise of the Transformer</h2>

<p>Like many others in tech, the launch of ChatGPT from OpenAI a few years ago was such an unexpected
but exciting development. I too jumped in and spent a lot of time playing with this new technology, 
but it really didn’t click at all outside of the fun. I couldn’t see an application of it that would 
solve real problems “for me”.</p>

<p>That persisted for at least a year. I subscribed for $20/month and continued to experiment.
I still found it incredibly interesting and a complete game changer, but most coding exercises 
were simply taking longer and leading me down strange paths. Don’t get 
me wrong the transformer architecture and capabilities were mind boggling, but it just didnt work 
“for me”.</p>

<h2 id="anthropic">Anthropic</h2>

<p>I tried Claude.ai from Anthropic and immediately subscribed. It seemed leaps ahead in my use case 
which was essentially typing out questions in the web UI, copying in code, and copying out answers. I 
really don’t like the code completion or IDE integrations yet, so while clunky, it works. 
The artefact model was also really well implemented. But still, it really wasn’t net positive “for me”.
I excitedly tried Claude Code and that was intriguing but left me running in circles.</p>

<h2 id="inflection-point">Inflection Point</h2>

<p>Claude Sonnet 4’s release has changed my workflow entirely. I have a couple of Rails side projects I’ve been 
working on and Claude Code has been just sensational. I’m not sure what happened but I’m honestly shocked.</p>

<p>Claude Code with Sonnet 4 is incredibly good at understanding what I’m trying to do when I interact 
like a pair. I’ve been powering through some new features at a phenomenal pace. I’ll stick to focused 
tasks, work in branches and not be afraid to roll back, but it’s really working well.
After initially being very doubtful, I’m convinced now this has changed my specific scenario
as an experienced Rails developer, I can talk through features, easily give feedback and suggest alternatives, 
suggest better patterns, and the pace is breathtaking.</p>

<p>Sure, Claude struggles and makes mistakes, but so do I, and I think this flow of pairing with Claude is 
the way “for me”. It seems to struggle most with tests - as do I - and sometimes makes stupid decisions that can 
distract eg it decided to override my log_in_as_user helper, and often adds JavaScript inline, but a 
quick correction “lets always use stimulus” is all that’s needed. Renaming a model is also a refactoring
that I’ve done so many times, but Claude handles it in seconds. And completely.</p>

<p><img src="/assets/images/articles/claude-code-usage.png" alt="Claude Code Usage June 2025" /></p>

<p>In the past when testing I’ve been annoyed within a short period of time, but in the last couple of days 
I’ve been flying. As you can see by the above, just the last few days is when this really clicked for me. 
My usage is up 10x from May, which was itself 10x from April.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>A bit of a different post, and just opinion, but I truly have seen a massive change in the last couple of 
months with Claude and for Rails development specifically, and I’m all in.</p>

<p>Would love to hear your experiences with LLM’s so don’t hesitate to drop me a note at my new favourite 
place <a href="https://bsky.app/profile/mileswoodroffe.com">Blue Sky</a> or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><category term="ai" /><summary type="html"><![CDATA[TL;DR]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rails Rate Limiting</title><link href="https://mileswoodroffe.com/articles/rails-rate-limiting" rel="alternate" type="text/html" title="Rails Rate Limiting" /><published>2024-12-16T00:00:00+00:00</published><updated>2024-12-16T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/rails-rate-limiting</id><content type="html" xml:base="https://mileswoodroffe.com/articles/rails-rate-limiting"><![CDATA[<h2 id="setting-the-scene">Setting the Scene</h2>

<p>I noticed a lot of new “users” in one of my side projects, and immediately wondered what was going on, was this it,
had I finally struck the startup gold?! Obviously I hadn’t, but nice to dream for a moment.</p>

<p>On closer inspection, I noticed a barrage of posts to my sign-in form with interesting user names like these:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"cmee7uvb'; waitfor delay '0:0:15' --"</span>,
<span class="s2">"0qjninbk' or 915=(select 915 from pg_sleep(15))--"</span>,
<span class="s2">"a77t30jc'; waitfor delay '0:0:15' --"</span>,
<span class="s2">"kxvmfhk6') or 273=(select 273 from pg_sleep(15))--"</span>,
<span class="s2">"a9vxyx7i' or 135=(select 135 from pg_sleep(15))--"</span>,
</code></pre></div></div>
<p>Aha! It’s a visit from our old friend Little Bobby Tables!</p>

<p><img src="https://imgs.xkcd.com/comics/exploits_of_a_mom.png" alt="xkcd.com - Little Bobby Tables" title="xkcd.com" />
<a href="https://xkcd.com/327/">xkcd.com</a></p>

<p>Ok! So now we can see someone with way too much time on their hands is attacking my little site for some nefarious 
reason or other. Just like Bobby Tables they’re probing and trying to use SQL-injection to cause some kind of mayhem. 
Thankfully I’m using Rails and the way Rails handles sanitisation of inputs to queries is already really solid.</p>

<p>Still it’s annoying, and it made me remember that there was a new Rate Limiting feature added to Rails 7.2 that I’d 
not tried out. Great opportunity to do that now.</p>

<h2 id="what-is-rate-limiting">What is Rate Limiting?</h2>

<p>In short, rate limiting is a way to control the number or rate of requests to a service, in my case, HTTP requests to 
my Rails app. This technique is often used as a first step to restrict the number of requests to a particular endpoint
from the same IP. It’s one of many ways to mitigate attacks, but the one we’re looking at today.</p>

<h2 id="the-rails-way">The Rails way</h2>

<p>Historically I would reach for <a href="https://github.com/rack/rack-attack">rack-attack</a> which is a very robust, battle-tested 
and configurable solution, but since Rails 7.2 we have an option for a simpler way, out of the box with an expressive 
syntax where we can just add <code class="language-plaintext highlighter-rouge">rate_limit</code> in a controller and be done!</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SessionsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">rate_limit</span> <span class="ss">to: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">within: </span><span class="mi">3</span><span class="p">.</span><span class="nf">minutes</span><span class="p">,</span> <span class="ss">only: :create</span>
<span class="k">end</span>
</code></pre></div></div>
<p>This one-liner is all we need to limit any IP to a maximum of 10 requests to our <code class="language-plaintext highlighter-rouge">create</code> action over a 3 minute period. 
delightful. On the 11th request, the server will return a “429 Too Many Requests” response.</p>

<p>We can add a bit more logic to the rule using the <code class="language-plaintext highlighter-rouge">by:</code> and <code class="language-plaintext highlighter-rouge">with:</code> parameters, where <code class="language-plaintext highlighter-rouge">by</code> takes a function to determine 
what is counted to be rate limited, and <code class="language-plaintext highlighter-rouge">with</code> lets us redirect to a specific location with another message. eg.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SignupsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="n">rate_limit</span> <span class="ss">to: </span><span class="mi">10</span><span class="p">,</span> <span class="ss">within: </span><span class="mi">3</span><span class="p">.</span><span class="nf">minutes</span><span class="p">,</span>
    <span class="ss">by: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">request</span><span class="p">.</span><span class="nf">domain</span> <span class="p">},</span> <span class="ss">with: </span><span class="o">-&gt;</span> <span class="p">{</span> <span class="n">redirect_to</span> <span class="n">busy_controller_url</span><span class="p">,</span> <span class="ss">alert: </span><span class="s2">"Too many signups on domain!"</span> <span class="p">},</span> <span class="ss">only: :new</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Lastly there’s a <code class="language-plaintext highlighter-rouge">store:</code> parameter you can pass in to specify the <code class="language-plaintext highlighter-rouge">ActiveSupport::Cache</code> cache store you want to use. It 
will default to use <code class="language-plaintext highlighter-rouge">config.cache_store</code> from your environment file. In this case, to keep it simple I’ve added 
<code class="language-plaintext highlighter-rouge">config.cache_store = :memory_store</code> to test.rb, development.rb and production.rb, but you can use any ActiveSupport::Cache 
backend.</p>

<h2 id="testing">Testing</h2>

<p>To confirm this works as we expect, lets add a simple Minitest test to confirm the rate limiting kicks in. The first one
POST’s an email address to our <code class="language-plaintext highlighter-rouge">SessionsController#create</code> action 10 times and asserts we get the expected redirect. It then
fires one more POST and we assert that we get the “too many requests” response.</p>

<p>The second test also POST’s to the same action 10 times, but then travels 3 minutes 1 second to the future and confirms 
it can make a further POST since our 3 minute window has now expired. Neat!</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">test</span> <span class="s2">"should enforce rate limit for create action"</span> <span class="k">do</span>
  <span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
    <span class="n">post</span> <span class="n">sessions_url</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"user</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">@example.com"</span> <span class="p">}</span>
    <span class="n">assert_response</span> <span class="ss">:redirect</span>
  <span class="k">end</span>
  
  <span class="n">post</span> <span class="n">sessions_url</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"overlimit@example.com"</span> <span class="p">}</span>
  <span class="n">assert_response</span> <span class="ss">:too_many_requests</span>
<span class="k">end</span>

<span class="nb">test</span> <span class="s2">"should reset rate limit after the time window"</span> <span class="k">do</span>
  <span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
    <span class="n">post</span> <span class="n">sessions_url</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"user</span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">@example.com"</span> <span class="p">}</span>
    <span class="n">assert_response</span> <span class="ss">:redirect</span>
  <span class="k">end</span>
  
  <span class="n">travel</span> <span class="mi">3</span><span class="p">.</span><span class="nf">minutes</span> <span class="o">+</span> <span class="mi">1</span><span class="p">.</span><span class="nf">second</span> <span class="k">do</span>
    <span class="n">post</span> <span class="n">sessions_url</span><span class="p">,</span> <span class="ss">params: </span><span class="p">{</span> <span class="ss">email: </span><span class="s2">"newuser@example.com"</span> <span class="p">}</span>
    <span class="n">assert_response</span> <span class="ss">:redirect</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>One mini gotcha is you may need to add <code class="language-plaintext highlighter-rouge">Rails.cache.clear</code> in your test_helper or setup block just to make sure the 
cache is reset for the tests.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>Short post this week, but hopefully of use. Don’t hesitate to drop me a note at my new favourite 
place <a href="https://bsky.app/profile/mileswoodroffe.com">Blue Sky</a> or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Setting the Scene]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/bobby-tables.webp" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/bobby-tables.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Solid Cable in Production with Kamal</title><link href="https://mileswoodroffe.com/articles/solid-cable-in-production" rel="alternate" type="text/html" title="Solid Cable in Production with Kamal" /><published>2024-12-02T00:00:00+00:00</published><updated>2024-12-02T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/solid-cable-in-production</id><content type="html" xml:base="https://mileswoodroffe.com/articles/solid-cable-in-production"><![CDATA[<h2 id="solid-cable-in-production-with-kamal">Solid Cable in Production with Kamal</h2>

<p>Last week I <a href="/articles/super-solid-cable">wrote about Solid Cable</a> and how amazingly easy it was to get up and running 
and add real time web socket magic to a Rails app. Rails 8 and the solid trifecta of 
Solid Cache, Solid Queue and Solid Cable, is a massive unlock to productivity, particularly 
evident when adding some of these historically pretty complicated capabilities and services: 
caching, job queues and web sockets respectively.</p>

<p>When I went to move this to production, I realised that there often aren’t that many 
blog posts or examples - including my own previous posts - that go into details beyond 
a demo on localhost. Lets fix that now! It was pretty straightforward, but a couple of 
tiny details caught me out, so wanted to capture those for next time!</p>

<h2 id="managing-multiple-databases-in-rails">Managing Multiple Databases in Rails</h2>

<p>In the <a href="/articles/super-solid-cable">previous post</a> we set up the “primary” and “cable” databases both with SQLite 
in the development environment as that’s the default in Rails 8, and also the simplest. 
But in my production app - and I suspect like many others - I was already using PostgreSQL as 
my main “primary” database. So we need to add SQLite in production to run alongside - in 
my case - Postgres. We configure database connections in <code class="language-plaintext highlighter-rouge">config/database.yml</code> so open 
this up and have a look at what you currently have.</p>

<p>In my case I had a <code class="language-plaintext highlighter-rouge">&amp;default</code> YAML anchor to set up the common configs, then a key each for 
<code class="language-plaintext highlighter-rouge">development</code>, <code class="language-plaintext highlighter-rouge">test</code> and <code class="language-plaintext highlighter-rouge">production</code> that pulls in the <code class="language-plaintext highlighter-rouge">&amp;default</code> config with a merge key 
and adds the environment specific configs in each as needed. I had postgresql 
set up for all three environments as shown below. You may have something different, 
like sqlite3 or mysql as your primary, but the general idea is the same.</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Existing database.yml</span>
<span class="na">default</span><span class="pi">:</span> <span class="nl">&amp;default</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">postgresql</span>
  <span class="na">encoding</span><span class="pi">:</span> <span class="s">unicode</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>
  <span class="na">pool</span><span class="pi">:</span> <span class="s">&lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;</span>

<span class="na">development</span><span class="pi">:</span>
  <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">myapp_development</span>

<span class="na">test</span><span class="pi">:</span>
  <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">myapp_test</span>

<span class="na">production</span><span class="pi">:</span>
  <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s">&lt;%= ENV["DB_HOST"] %&gt;</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_DB"] %&gt;</span>
  <span class="na">username</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_USER"] %&gt;</span>
  <span class="na">password</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_PASSWORD"] %&gt;</span>
</code></pre></div></div>

<p>Ok. So now to use Solid Cable with SQLite while keeping PostgreSQL for the main app database we 
need to update <code class="language-plaintext highlighter-rouge">database.yml</code> to support multiple databases ie. SQLite as well as our existing 
PostgreSQL. This capability was introduced in Rails 6 I think, and you can read all about it 
in the excellent <a href="https://guides.rubyonrails.org/active_record_multiple_databases.html">Rails Guides</a> if you want to get some more background.</p>

<p>Firstly, lets add a new YAML anchor for sqlite to <code class="language-plaintext highlighter-rouge">config/database.yml</code> as shown here. Put it 
just below the existing <code class="language-plaintext highlighter-rouge">&amp;default</code> anchor block.</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">sqlite</span><span class="pi">:</span> <span class="nl">&amp;sqlite</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">sqlite3</span>
  <span class="na">pool</span><span class="pi">:</span> <span class="s">&lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;</span>
  <span class="na">timeout</span><span class="pi">:</span> <span class="m">5000</span>
</code></pre></div></div>
<p>When using multiple databases in an environment, we can specify them by adding a configuration 
key for each under the environment key. Here’s how my <code class="language-plaintext highlighter-rouge">database.yml</code> file looked after adding the 
<code class="language-plaintext highlighter-rouge">&amp;sqlite</code> anchor and then adding <code class="language-plaintext highlighter-rouge">primary</code> and <code class="language-plaintext highlighter-rouge">cable</code> configurations for each environment:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">default</span><span class="pi">:</span> <span class="nl">&amp;default</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">postgresql</span>
  <span class="na">encoding</span><span class="pi">:</span> <span class="s">unicode</span>
  <span class="na">port</span><span class="pi">:</span> <span class="m">5432</span>
  <span class="na">pool</span><span class="pi">:</span> <span class="s">&lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;</span>

<span class="na">sqlite</span><span class="pi">:</span> <span class="nl">&amp;sqlite</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">sqlite3</span>
  <span class="na">pool</span><span class="pi">:</span> <span class="s">&lt;%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %&gt;</span>
  <span class="na">timeout</span><span class="pi">:</span> <span class="m">5000</span>

<span class="na">development</span><span class="pi">:</span>
  <span class="na">primary</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">myapp_development</span>
  <span class="na">cable</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*sqlite</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">storage/myapp_development_cable.sqlite3</span>
    <span class="na">migrations_paths</span><span class="pi">:</span> <span class="s">db/cable_migrate</span>

<span class="na">test</span><span class="pi">:</span>
  <span class="na">primary</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">myapp_test</span>
  <span class="na">cable</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*sqlite</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">storage/myapp_test_cable.sqlite3</span>
    <span class="na">migrations_paths</span><span class="pi">:</span> <span class="s">db/cable_migrate</span>

<span class="na">production</span><span class="pi">:</span>
  <span class="na">primary</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
    <span class="na">host</span><span class="pi">:</span> <span class="s">&lt;%= ENV["DB_HOST"] %&gt;</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_DB"] %&gt;</span>
    <span class="na">username</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_USER"] %&gt;</span>
    <span class="na">password</span><span class="pi">:</span> <span class="s">&lt;%= ENV["POSTGRES_PASSWORD"] %&gt;</span>
  <span class="na">cable</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*sqlite</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">/data/myapp_production_cable.sqlite3</span>
    <span class="na">migrations_paths</span><span class="pi">:</span> <span class="s">db/cable_migrate</span>
</code></pre></div></div>

<p>We’re only really interested in the “production” settings in the context of this post but I’ve 
left the rest in for completeness.</p>

<p>So, lets go over this line by line:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">primary:</code> specifies the primary database. In our case that’s our existing “main” application 
database. It should always be named “primary” to keep things clear.</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;&lt;: *default</code> is a YAML merge key that pulls in the configuration from the <code class="language-plaintext highlighter-rouge">&amp;default</code> anchor</li>
  <li><code class="language-plaintext highlighter-rouge">host</code>, <code class="language-plaintext highlighter-rouge">database</code>, <code class="language-plaintext highlighter-rouge">username</code> and <code class="language-plaintext highlighter-rouge">password</code> obviously are the location, name and auth credentials
for your database. Best practice is to pass these in from environment variables as shown</li>
  <li><code class="language-plaintext highlighter-rouge">cable</code> is the configuration key for our Solid Cable SQLite instance! It’s important that we 
name it “cable” so it matches with the name in <code class="language-plaintext highlighter-rouge">config/cable.yml</code></li>
  <li><code class="language-plaintext highlighter-rouge">&lt;&lt;: *sqlite</code> is another YAML merge key that pulls in the config from the <code class="language-plaintext highlighter-rouge">&amp;sqlite</code> anchor</li>
  <li><code class="language-plaintext highlighter-rouge">database</code> pay close attention here! this is the path to the SQLite database file in production. 
In my case I have this in a volume mounted by Kamal. More on that later</li>
  <li><code class="language-plaintext highlighter-rouge">migrations_paths</code> is the path in our codebase where we can add migrations specific to the SQL 
database</li>
</ul>

<h2 id="solid-cable-configuration">Solid Cable Configuration</h2>

<p>Nothing to change here but as a reminder, Solid Cable is configured in <code class="language-plaintext highlighter-rouge">config/cable.yml</code> and 
important as mentioned above that the production database configuration key for the SQLite 
database we’re using for Solid Cable is named the same here. ie “cable”</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">production</span><span class="pi">:</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">solid_cable</span>
  <span class="na">connects_to</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span>
      <span class="na">writing</span><span class="pi">:</span> <span class="s">cable</span>
  <span class="na">polling_interval</span><span class="pi">:</span> <span class="s">0.1.seconds</span>
  <span class="na">message_retention</span><span class="pi">:</span> <span class="s">1.day</span>
</code></pre></div></div>

<h2 id="kamal-sqlite-location">Kamal SQLite Location</h2>

<p>As I’ve written about numerous times in the past like <a href="/articles/deploying-with-kamal">here</a> 
and <a href="/articles/kamal-2-upgrade">here</a>, I’m all in on Kamal! It’s a fantastic tool for deploying web apps. 
Previously I had only used PostgreSQL and only one database per app. Now we need to add 
SQLite to the mix. Fortunately it’s super simple, but I had one gotcha that took some 
fiddling to get right and wanted to capture here.</p>

<p>As you saw above, in the <code class="language-plaintext highlighter-rouge">database.yml</code> file we need to specify the name and location of the 
SQLite database file. The Rails default is to set this to <code class="language-plaintext highlighter-rouge">database: storage/production_cable.sqlite3</code> 
which makes sense as a default.</p>

<p>In our case with Kamal, we want to make sure that this database persists across deployments, so 
we need to make sure it’s mounted on the server’s file system outside the docker container 
but still accessible from the container each time it’s deployed with Kamal.</p>

<p>Fortunately this is really easy too with the <code class="language-plaintext highlighter-rouge">volumes</code> key in <code class="language-plaintext highlighter-rouge">config/deploy.yml</code>. With this 
setting, Kamal will automatically make a persistent storage volume at <code class="language-plaintext highlighter-rouge">/data</code> on our server.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">## config/deploy.yml</span>
<span class="na">volumes</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">myapp_data:/data"</span>
</code></pre></div></div>
<p>Then we need to make sure we are using this volume in the <code class="language-plaintext highlighter-rouge">database.yml</code> config so that Rails 
knows where to find the file. In my case above it’s <code class="language-plaintext highlighter-rouge">database: /data/myapp_production_cable.sqlite3</code></p>

<p>The embarrassing “gotcha” I mentioned above was I was missing <code class="language-plaintext highlighter-rouge">/</code> in front of <code class="language-plaintext highlighter-rouge">data</code> so Rails was 
trying to put the database in the relative path <code class="language-plaintext highlighter-rouge">/rails/data</code> and not the volume I had mounted 
at <code class="language-plaintext highlighter-rouge">/data</code>! Very annoying but didn’t take long to realise :)</p>

<h2 id="deploying">Deploying</h2>

<p>Finally we just need to deploy the app! Kamal always runs <code class="language-plaintext highlighter-rouge">rails db:prepare</code> which will create 
our SQLite database the first time and that should be it! Solid Cable in production</p>

<h2 id="verifying">Verifying</h2>

<p>To double check our SQLite instance is up and running and brokering our web socket messages 
as we expect, we can jump in to the console in production to see if we have any messages. Make 
sure to trigger an event in your app first and then check if the message was persisted:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal app <span class="nb">exec</span> <span class="nt">-i</span> <span class="s1">'bin/rails console'</span>

<span class="o">&gt;</span> SolidCable::Message.last
<span class="c">#&lt;SolidCable::Message:0x00007ff53fe4b8f8</span>
 <span class="nb">id</span>: 12,
 channel: <span class="s2">"item_106"</span>,
 payload: <span class="s2">"</span><span class="se">\"\\</span><span class="s2">u003cturbo-stream action=</span><span class="se">\\\"</span><span class="s2">prepend</span><span class="se">\\\"</span><span class="s2"> target=</span><span class="se">\\\"</span><span class="s2">un..."</span>,
 created_at: <span class="s2">"2024-11-30 12:26:30.443258000 +0000"</span>,
 channel_hash: <span class="nt">-7306872458338484105</span><span class="o">&gt;</span>
</code></pre></div></div>
<p>Lovely! Here we can see a payload was captured to prepend some content on channel “item_106”.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>OK! I didn’t find any articles specifically on actually deploying Solid Cable to production with 
SQLite while maintaining the primary database on PostgreSQL which prompted me to write this one. 
Future me thanks myself at least.</p>

<p>Don’t hesitate to drop me a note at my new favourite place <a href="https://bsky.app/profile/mileswoodroffe.com">Blue Sky</a> or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Solid Cable in Production with Kamal]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/database-yml.webp" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/database-yml.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Super Solid Cable</title><link href="https://mileswoodroffe.com/articles/super-solid-cable" rel="alternate" type="text/html" title="Super Solid Cable" /><published>2024-11-27T00:00:00+00:00</published><updated>2024-11-27T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/super-solid-cable</id><content type="html" xml:base="https://mileswoodroffe.com/articles/super-solid-cable"><![CDATA[<h2 id="solid-cable">Solid Cable</h2>

<p>Solid Cable is a new adapter for Action Cable - now the default in Rails 8 - that 
brings the magic of web sockets and real time updates to your Rails app.</p>

<p>I’d never had chance to really use Action Cable before, but a colleague gave me a great 
suggestion to improve an internal tool I’ve been building, and that coincided with me 
having just reviewed some upcoming videos for the Rails Foundation - including solid 
cable - and a 90 minute train journey from Bristol to London!</p>

<p>How far could I get introducing real time updates to my app on a train to London I wondered? 
What could go wrong? Well, Solid Cable is so ridiculously simple that I’d finished before I 
even got to Paddington! Now en-route home on the same journey, I thought I would write up a 
blog post, as always, for future me. So, here we go.</p>

<h2 id="simple-demo-app">Simple Demo App</h2>

<p>Just to keep things really simple, lets create a new Rails 8 app that lets people chat. Here’s 
the simplest thing I could come up with but will be perfect to illustrate how to use Solid Cable.</p>

<p><img src="/assets/images/articles/simple-chat.webp" alt="A Simple Chat App" /></p>

<p>First up you’ll need Ruby and Rails 8 on your machine. If you don’t yet, follow the steps 
in section 3.1 of the fantastic, newly polished and updated Rails Guides <a href="https://guides.rubyonrails.org/getting_started.html#creating-a-new-rails-project-installing-rails">here</a></p>

<p>OK, lets go ahead and create our new Rails app:</p>
<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails new super-simple-chat <span class="nt">-c</span> tailwind
</code></pre></div></div>
<p>Next, we’re going to create a model, a controller and some views to play with. Initially we’ll use standard
rails and the app will be feature complete. First we’ll <code class="language-plaintext highlighter-rouge">cd</code> into the app directory and generate 
the model, controller and index view by running these commands on the console:</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># change into the app directory</span>
<span class="nb">cd </span>super-simple-chat

<span class="c"># generate the Chat model and controller</span>
rails g model chat message:string
rails g controller chats index create

<span class="c"># migrate the database</span>
rails db:migrate
</code></pre></div></div>

<p>We need to add a bit of code here, and I won’t explain this in detail since it’s very basic Rails. 
I’ll save the more detailed descriptions for when we add the real time Solid Cable magic coming up. So, open up 
each of these files and replace the existing code in each with the code below:</p>

<p><code class="language-plaintext highlighter-rouge">config/routes.rb</code></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">routes</span><span class="p">.</span><span class="nf">draw</span> <span class="k">do</span>
  <span class="n">resources</span> <span class="ss">:chats</span>
  <span class="n">root</span> <span class="s2">"chats#index"</span>
<span class="k">end</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/controllers/chats_controller.rb</code></p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">ChatsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">index</span>
    <span class="vi">@chats</span> <span class="o">=</span> <span class="no">Chat</span><span class="p">.</span><span class="nf">all</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">create</span>
    <span class="vi">@chat</span> <span class="o">=</span> <span class="no">Chat</span><span class="p">.</span><span class="nf">build</span><span class="p">(</span><span class="n">chat_params</span><span class="p">)</span>
    <span class="vi">@chat</span><span class="p">.</span><span class="nf">save</span>

    <span class="n">redirect_to</span> <span class="n">chats_path</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">chat_params</span>
    <span class="n">params</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:chat</span><span class="p">).</span><span class="nf">permit</span><span class="p">(</span><span class="ss">:message</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/views/chats/index.html.erb</code></p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"w-1/2"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h1</span> <span class="na">class=</span><span class="s">"mb-6 text-slate-700 font-bold text-2xl"</span><span class="nt">&gt;</span>Chats<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"space-y-4 mb-8"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"chats"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="vi">@chats</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="s2">"form"</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>
<p>And then we need to create two partials, one for the input form, and one for the individual
chat messages:</p>

<p><code class="language-plaintext highlighter-rouge">app/views/chats/_form.html.erb</code></p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">form_with</span> <span class="ss">model: </span><span class="no">Chat</span><span class="p">.</span><span class="nf">new</span><span class="p">,</span> <span class="ss">local: </span><span class="kp">false</span> <span class="k">do</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:message</span><span class="p">,</span>
        <span class="ss">autocomplete: :off</span><span class="p">,</span>
        <span class="ss">class: </span><span class="s2">"block w-2/3 rounded-md bg-white px-3 py-1.5 text-base text-gray-900 outline outline-1 -outline-offset-1 outline-gray-300 placeholder:text-gray-400"</span><span class="p">,</span>
        <span class="ss">placeholder: </span><span class="s2">"enter message..."</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">submit</span> <span class="s2">"Send"</span><span class="p">,</span>
        <span class="ss">class: </span><span class="s2">"inline-flex w-1/3 items-center justify-center rounded-md bg-slate-400 ml-3 px-3 py-2 text-sm font-semibold text-white shadow-sm"</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">app/views/chats/_chat.html.erb</code></p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"flex justify-between items-start"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-gray-700"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">chat</span><span class="p">.</span><span class="nf">message</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"text-sm text-gray-400"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">time_ago_in_words</span><span class="p">(</span><span class="n">chat</span><span class="p">.</span><span class="nf">created_at</span><span class="p">)</span> <span class="cp">%&gt;</span> ago
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>
<h2 id="the-basic-app">The Basic App</h2>

<p>OK! If you followed the instructions above, you should be able to run the app with <code class="language-plaintext highlighter-rouge">bin/dev</code> in the 
console and visit <a href="http://localhost:3000">http://localhost:3000</a>. It should look like the 
screenshot above, and you can type in chats and see them rendered above the input box. Try it out!</p>

<p>This actually works great, and shows the power of Rails out of the box. Whats happening 
is the text input is posted to the server and the page is reloaded rendering the list of messages.</p>

<p>If someone else sends a message from another server though, we won’t see it unless we reload the page. We 
want to see these messages in real time, that’s how we roll, so lets bring the magic with Solid Cable!</p>

<h2 id="introducing-solid-cable">Introducing Solid Cable</h2>

<p>We’ve built the basics, and this is essentially where I was with my app. To introduce Solid Cable 
we need to have the gem in our Gemfile and some minor configuration set up.</p>

<p>If you’re using Rails 8.0.0, you’re in luck. You don’t need to do anything! Solid Cable is available by 
default. If you don’t have Solid Cable for some reason, like an older version of Rails, you’ll need to 
add the gem and install as follows:</p>
<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add solid_cable
bin/rails solid_cable:install
</code></pre></div></div>

<h2 id="configuring-solid-cable">Configuring Solid Cable</h2>

<p>The main configuration file for Action Cable is <code class="language-plaintext highlighter-rouge">config/cable.yml</code> and by default in “development” the 
adapter is set to “async”. Since we want to enable Solid Cable on localhost, we need to update this 
file, so open it up and change:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">development</span><span class="pi">:</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">async</span>
</code></pre></div></div>
<p>to:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">development</span><span class="pi">:</span>
  <span class="na">adapter</span><span class="pi">:</span> <span class="s">solid_cable</span>
  <span class="na">connects_to</span><span class="pi">:</span>
    <span class="na">database</span><span class="pi">:</span>
      <span class="na">writing</span><span class="pi">:</span> <span class="s">cable</span>
  <span class="na">polling_interval</span><span class="pi">:</span> <span class="s">0.1.seconds</span>
  <span class="na">message_retention</span><span class="pi">:</span> <span class="s">1.day</span>
</code></pre></div></div>
<p>Then lastly we need to set up a cable database for Solid Cable to store our messages. Unlike the 
typical way Action Cable is used where events are published to Redis then immediately 
broadcast back to Action Cable with a pub/sub strategy, with Solid Cable, messages are written to a database 
and Solid Cable polls for new ones, broadcasting them back to clients when found. Solid Cable also cleans 
up after itself, trimming data older than <code class="language-plaintext highlighter-rouge">message_retention</code> above.</p>

<p>So we need to set up a new database to accept the events from Solid Cable. SQLite is perfect for this task 
so similar to the cable config above, we need to set up the “cable” database in our development environment. 
Open up the <code class="language-plaintext highlighter-rouge">config/database.yml</code> file and change the following:</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">development</span><span class="pi">:</span>
  <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
  <span class="na">database</span><span class="pi">:</span> <span class="s">storage/development.sqlite3</span>
</code></pre></div></div>
<p>to</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">development</span><span class="pi">:</span>
  <span class="na">primary</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">storage/development.sqlite3</span>
  <span class="na">cable</span><span class="pi">:</span>
    <span class="na">&lt;&lt;</span><span class="pi">:</span> <span class="nv">*default</span>
    <span class="na">database</span><span class="pi">:</span> <span class="s">storage/development_cable.sqlite3</span>
    <span class="na">migrations_paths</span><span class="pi">:</span> <span class="s">db/cable_migrate</span>
</code></pre></div></div>
<p>Now lets initialise the database with <code class="language-plaintext highlighter-rouge">rails db:prepare</code></p>

<h2 id="unleash-the-web-sockets">Unleash the Web Sockets</h2>

<p>OK! Back to our app now. We’re going to add some real time web socket goodness with just a few lines 
of code!</p>

<p>Firstly we need to web socket enable our view so that newly broadcasted Chats can be rendered. Open 
up the <code class="language-plaintext highlighter-rouge">app/views/chats/index.html.erb</code> file and insert the <code class="language-plaintext highlighter-rouge">turbo_stream_from</code> tag on line 4 as shown here</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"w-1/2"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h1</span> <span class="na">class=</span><span class="s">"mb-6 text-slate-700 font-bold text-2xl"</span><span class="nt">&gt;</span>Chats<span class="nt">&lt;/h1&gt;</span>
  <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"space-y-4 mb-8"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">turbo_stream_from</span> <span class="s2">"chats"</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"chats"</span><span class="nt">&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="vi">@chats</span> <span class="cp">%&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
  <span class="nt">&lt;/div&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">render</span> <span class="s2">"form"</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>And then we just need to update our Chat model to broadcast when a new Chat is created. Open up 
<code class="language-plaintext highlighter-rouge">app/models/chat.rb</code> and replace the code with the following:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Chat</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">after_create_commit</span> <span class="ss">:broadcast_chat</span>

  <span class="k">def</span> <span class="nf">broadcast_chat</span>
    <span class="n">broadcast_append_to</span><span class="p">(</span>
      <span class="s2">"chats"</span><span class="p">,</span>
      <span class="ss">target: </span><span class="s2">"chats"</span><span class="p">,</span>
      <span class="ss">partial: </span><span class="s2">"chats/chat"</span><span class="p">,</span>
      <span class="ss">locals: </span><span class="p">{</span> <span class="ss">chat: </span><span class="nb">self</span> <span class="p">}</span>
    <span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>One last tidy up is we don’t need to redirect to the index page in the controller after a Chat is created.
We’ll be updating the page via websockets, so we can just return nothing. In the 
<code class="language-plaintext highlighter-rouge">app/controllers/chats_controller.rb</code> file “create” action, simple replace the line</p>

<p><code class="language-plaintext highlighter-rouge">redirect_to chats_path</code></p>

<p>with</p>

<p><code class="language-plaintext highlighter-rouge">render json: {}, status: :no_content</code></p>

<p>Now you may not believe this. But thats it. You’re done! Restart your server, then open up two browsers 
and behold the magic! When you send a chat in one, you should see it in the other in real time, no refresh required!</p>

<video width="640" height="360" controls="">
  <source src="/assets/images/articles/solid-cable-powered-chat.mov" type="video/quicktime" />
  Your browser does not support the video tag.
</video>

<h2 id="under-the-hood">Under the Hood</h2>

<p>If you take a look in your Rails log you should be able to see not only the Chat record being inserted 
when a chat message is sent, but also the event being inserted in the cable database. It will looks 
something like this:</p>

<pre><code class="language-log">SolidCable::Message Insert (0.5ms)  INSERT INTO "solid_cable_messages" ("created_at","channel",
"payload","channel_hash") VALUES ('2024-11-26 06:23:40.882477', x'6368617473', x'225c753030336
3747572626f2d73747265616d20616374696f6e3d5c22617070656e645c22207461726765743d5c2263686174735c2
25c75303033655c753030336374656d706c6174655c75303033655c7530303363212d2d20424547494e206170702f7
6696577732f63686174732f5f636861742e68746d6c2e657262202d2d5c75303033655c75303033636469765c75303
033655c6e20205c753030336364697620636c6173733d5c22666c6578206a7573746966792d6265747765656e20697
f6469765c75303033655c6e202020205c753030336364697620636c6173733d5c22746578742d736d20746578742d6
77261792d3430305c225c75303033655c6e2020202020206c657373207468616e2061206d696e7574652061676f5c6
e202020205c75303033632f6469765c75303033655c6e20205c75303033632f6469765c75303033655c6e5c7530303
3632f6469765c75303033655c6e5c7530303363212d2d20454e44206170702f76696577732f63686174732f5f63686
1742e68746d6c2e657262202d2d5c75303033655c75303033632f74656d706c6174655c75303033655c75303033632
f747572626f2d73747265616d5c753030336522', -2034957492849835134) ON CONFLICT  DO NOTHING RETURNING
"id" /*action='create',application='SuperSolidCable',controller='chats'*/
</code></pre>

<p>and then also you can see our partial being broadcast out to clients with the “append” action</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Turbo::StreamsChannel transmitting "&lt;turbo-stream action=\"append\" target=\"chats\"&gt;&lt;template&gt;
&lt;!-- BEGIN app/views/chats/_chat.html.erb --&gt;&lt;div&gt;\n  &lt;div class=\"flex justify-between items-
start\"&gt;\n    &lt;div class=\"text-gray-700\"&gt;\n      solid cable\n    &lt;/div&gt;\n    &lt;div class=\"te
xt-sm text-gray-400\"&gt;\n      less than a minute... (via streamed from chats)
</code></pre></div></div>
<p>Pretty damn cool!</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>This is quite a long blog post to demonstrate how you can add web sockets and real time updates 
in your Rails apps with a few lines of code, I know, but hopefully its been fun.</p>

<p>As always, oh wait, not as always as I moved to Blue Sky mostly, but yes, don’t hesitate to drop me a note 
there at <a href="https://bsky.app/profile/mileswoodroffe.com">https://bsky.app/profile/mileswoodroffe.com</a> 
or via any channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Solid Cable]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/simple-chat.webp" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/simple-chat.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Upgrading to Kamal 2</title><link href="https://mileswoodroffe.com/articles/kamal-2-upgrade" rel="alternate" type="text/html" title="Upgrading to Kamal 2" /><published>2024-10-09T00:00:00+00:00</published><updated>2024-10-09T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/kamal-2-upgrade</id><content type="html" xml:base="https://mileswoodroffe.com/articles/kamal-2-upgrade"><![CDATA[<h2 id="kamal">Kamal</h2>

<p>Back in February <a href="/articles/deploying-with-kamal">I wrote about</a> an exciting new deployment tool called Kamal announced by
DHH and 37signals. It really was a great gift and I quickly switched my personal projects over to 
Kamal and hosting on cheap VPS’s a Hetzner. The cool-aid was flowing and it tasted.. well, great!</p>

<p>It was a bit fiddly to get working and had a few limitations - such as deploying multiple apps 
on one host was a challenge - but for my use case it was perfect. Been using it ever since.</p>

<h2 id="kamal-2">Kamal 2</h2>

<p>Just in time for Rails World 2024, Donal from 37signals <a href="https://dev.37signals.com/kamal-2/">announced Kamal 2</a> 
which introduced some key enhancements:</p>

<ul>
  <li>kamal-proxy that replaces traefik</li>
  <li>automatic HTTPS and provisioning with Let’s Encrypt</li>
  <li>support for multiple apps on one server</li>
  <li>command aliases</li>
  <li>move away from .env for secrets</li>
</ul>

<h2 id="upgrading-to-kamal-2">Upgrading to Kamal 2</h2>

<p>I spent some time cautiously setting up a beta subdomain and deployed a second instance of my app to fresh 
servers just for testing the upgrade, but it was unbelievably straightforward. So much so, I was 
convinced it hadn’t changed anything but yep, it was that seamless!</p>

<p>The <a href="https://kamal-deploy.org/docs/upgrading/overview/">official upgrade guide</a> is likely all you’ll need
but I made the following brief notes to capture some nuances that tripped me up, and mainly to remind future me, 
but hopefully may help someone who stumbles on this page in the future.</p>

<h3 id="1-upgrade-the-kamal-gem">1. Upgrade the Kamal gem</h3>

<p>First things first, we need to update the gem! The guide tells us to make sure we have v1.9.0 or install if not,
and then confirm we can do a deploy, since this is the first version that can reverse the upgrade we’re about 
to perform.</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>kamal <span class="nt">--version</span> 1.9.0
</code></pre></div></div>

<p>Once we’ve deployed to confirm its all still working, we now can upgrade kamal to the latest. At the time of 
writing this is 2.1.1 but lets get the latest</p>

<div class="language-zsh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem <span class="nb">install </span>kamal
</code></pre></div></div>

<h3 id="2-deployyml-configuration-changes">2. Deploy.yml Configuration Changes</h3>

<p>OK! Now we’ve tested 1.9.0 and have a way back, and then installed the latest 2.x version of Kamal we’re ready to 
update the configurations.</p>

<p>Firstly we need to specify the architecture we’re building for. Remember Kamal uses Docker, and in my case its <code class="language-plaintext highlighter-rouge">amd64</code>. 
You can still do multi-architecture builds by adding another attribute eg <code class="language-plaintext highlighter-rouge">arm64</code> but in my case, I dont need that.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">builder</span><span class="pi">:</span>
  <span class="na">arch</span><span class="pi">:</span> <span class="s">amd64</span>
</code></pre></div></div>

<p>The big change in Kamal 2 is <code class="language-plaintext highlighter-rouge">kamal-proxy</code> coming in to replace Traefik. So, we can remove all the previous configurations
we had for traefik which in my case looked like this:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Remove everything under `traefik:`</span>
<span class="c1"># traefik:</span>
<span class="c1">#   options:</span>
<span class="c1">#     publish:</span>
<span class="c1">#       - 443:443</span>
<span class="c1">#   args:</span>
<span class="c1">#     entryPoints.websecure.address: ":443"</span>
<span class="c1">#     api: true</span>
<span class="c1">#   labels:</span>
<span class="c1">#     traefik.http.routers.dashboard.rule: ...</span>
</code></pre></div></div>
<p>Having removed the traefik config above, we need to configure kamal-proxy. Fortunately this is super simple. Add 
the following, ensuring to substitute in your-domain of course:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">proxy</span><span class="pi">:</span>
  <span class="na">ssl</span><span class="pi">:</span> <span class="kc">true</span>
  <span class="na">host</span><span class="pi">:</span> <span class="s">your-domain.com</span>
</code></pre></div></div>

<p>Some notes here. The docs were a bit confusing since setting <code class="language-plaintext highlighter-rouge">ssl: true</code> seemingly sets up Let’s Encrypt to issue 
an SSL cert, but only when you have one server.</p>

<p>In my case I’m using Cloudflare for SSL/TLS encryption, and despite reading I should set <code class="language-plaintext highlighter-rouge">ssl: false</code>,
after much fiddling around, I found I did indeed need to set it to <code class="language-plaintext highlighter-rouge">true</code> and make sure Cloudflare Encryption 
Mode was set to “Full”.</p>

<p>The other gotcha for me was with Kamal 1.x traefik accepted the incoming connections on port 443 and expected the 
Rails server to be running on port 3000.</p>

<p>The default behaviour for Kamal 2.x and kamal-proxy is it to connect to port 80 on the Rails server. This is due to 
the addition of Thruster - more on that in a minute - but for now just know if you don’t plan to add Thruster, you 
need to tell the proxy to use port 3000 by adding <code class="language-plaintext highlighter-rouge">app_port: 3000</code> under the <code class="language-plaintext highlighter-rouge">proxy:</code> key.</p>

<p>Lastly for the deploy.yml, you can optionally add aliases in Kamal 2.x. This is pretty handy. I was always looking 
up how to connect to the rails console on my app server for example. These are the defaults, but feel free to add
your own too as needed</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">aliases</span><span class="pi">:</span>
  <span class="na">console</span><span class="pi">:</span> <span class="s">app exec --interactive --reuse "bin/rails console"</span>
  <span class="na">shell</span><span class="pi">:</span> <span class="s">app exec --interactive --reuse "bash"</span>
  <span class="na">logs</span><span class="pi">:</span> <span class="s">app logs -f</span>
  <span class="na">dbc</span><span class="pi">:</span> <span class="s">app exec --interactive --reuse "bin/rails dbconsole"</span>
</code></pre></div></div>
<p>So now you can just enter <code class="language-plaintext highlighter-rouge">kamal console</code> on the command line and boom, you’re dropped right into the console!</p>

<h3 id="3-move-env-to-kamalsecrets">3. Move .env to .kamal/secrets</h3>

<p>Next we need to update our secrets! Previously in Kamal 1.x it simply used the <code class="language-plaintext highlighter-rouge">.env</code> file for secrets which is 
really convenient but somewhat fragile as they serve other purposes too.</p>

<p>In Kamal 2.x a new <code class="language-plaintext highlighter-rouge">.kamal/secrets</code> file was introduced specifically for your secrets. The easiest way if you’re 
upgrading is to reference the env vars in <code class="language-plaintext highlighter-rouge">.env</code> that you need for your app. Create the file <code class="language-plaintext highlighter-rouge">.kamal/secrets</code> 
and add the following:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">KAMAL_REGISTRY_PASSWORD</span><span class="o">=</span><span class="nv">$KAMAL_REGISTRY_PASSWORD</span>
<span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat </span>config/master.key<span class="si">)</span>
<span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span><span class="nv">$POSTGRES_PASSWORD</span>
<span class="nv">S3_ACCESS_KEY_ID</span><span class="o">=</span><span class="nv">$S3_ACCESS_KEY_ID</span>
<span class="nv">S3_SECRET_ACCESS_KEY</span><span class="o">=</span><span class="nv">$S3_SECRET_ACCESS_KEY</span>
</code></pre></div></div>

<p>One gotcha: I found I needed to prefix kamal commands with dotenv now to have the secrets available, 
so just fyi if you have issues with secrets, it may be this! eg. <code class="language-plaintext highlighter-rouge">dotenv kamal deploy</code></p>

<h3 id="4-thruster">4. Thruster</h3>

<p>Thruster wraps Puma and provides some great features like Basic HTTP Caching, X-Sendfile support and compression, 
HTTP/2 support and Automatic TLS certificate management with Let’s Encrypt</p>

<p>It so simple to add, and since it default in Rails 8, lets go for it. First we need to add the gem:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bundle add thruster
</code></pre></div></div>
<p>Next, if you’re not using Rails 8 yet, you’ll need to add the command script, so create a new file <code class="language-plaintext highlighter-rouge">bin/thrust</code> and 
copy in the following, being sure to set the permission on that file after creating it with <code class="language-plaintext highlighter-rouge">chmod 755 bin/thrust</code>
from your root directory:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/usr/bin/env ruby</span>
require <span class="s2">"rubygems"</span>
require <span class="s2">"bundler/setup"</span>

load Gem.bin_path<span class="o">(</span><span class="s2">"thruster"</span>, <span class="s2">"thrust"</span><span class="o">)</span>
</code></pre></div></div>

<p>Lastly we need to configure Docker to fire up Thruster by updating <code class="language-plaintext highlighter-rouge">Dockerfile</code> as below. Replace the first 
three lines with the second three lines:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gd">- # Start the server by default, this can be overwritten at runtime
- EXPOSE 3000
- CMD ["./bin/rails", "server"]
</span><span class="gi">+ # Start server via Thruster by default, this can be overwritten at runtime
+ EXPOSE 80
+ CMD ["./bin/thrust", "./bin/rails", "server"]
</span></code></pre></div></div>
<p>As you can see, this is where we’re setting Rails to run on port 80 as mentioned in step 2 above.</p>

<h3 id="5-run-the-upgrade-command">5. Run the upgrade command</h3>

<p>OK!! Now we’re ready to rumble. <strong>WARNING</strong> run this on a test server first!</p>

<p>Here we go. Simply run the following to initiate the in-place upgrade process</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal upgrade
</code></pre></div></div>

<p>This will remove traefik from your containers, add kamal-proxy and then reboot the current app.</p>

<p>Amazingly in my case, it Just Worked(TM)! I was so surprised I had to double check processes on the server
to make sure, but yes, worked first time!</p>

<h3 id="6-rolling-back">6. Rolling Back</h3>

<p>If things didn’t go to plan, since we installed 1.9.0 (hopefully you did) then we can rollback.</p>

<p>Firstly, lets uninstall kamal 2.x. Make sure to set the version to the one you installed. In my case it was 2.1.1</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gem uninstall kamal <span class="nt">-v</span> 2.1.1
</code></pre></div></div>
<p>Now lets check you have kamal 1.9.0 ready. Running the command below should return the output <code class="language-plaintext highlighter-rouge">1.9.0</code></p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal version
</code></pre></div></div>
<p>Next you need to revert the changes to deploy.yml and Dockerfile from steps 2 and 3 above, and then finally we 
can run the downgrade command that removes kamal-proxy and adds traefik back to your containers</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kamal downgrade
</code></pre></div></div>

<h2 id="wrapping-up">Wrapping up</h2>

<p>This is a really niche post. Only helpful if you a) installed Kamal 1.0 and b) for some reason don’t want to 
follow the official guides, but I like to document this stuff if only for myself. If it helps someone else. That’s
a bonus!</p>

<p>As always, any questions of feedback, don’t hesitate to drop me a note on <a href="https://twitter.com/tapster">twitter</a> or via any other channels listed <a href="/about">here</a></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Kamal]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/dockerfile-2.webp" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/dockerfile-2.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rails World 2024!</title><link href="https://mileswoodroffe.com/articles/rails-world-2024" rel="alternate" type="text/html" title="Rails World 2024!" /><published>2024-09-29T00:00:00+00:00</published><updated>2024-09-29T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/rails-world-2024</id><content type="html" xml:base="https://mileswoodroffe.com/articles/rails-world-2024"><![CDATA[<h2 id="rails-world-2024">Rails World 2024</h2>

<p>I’m running out of superlatives! This time last year I was euphorically <a href="/articles/rails-world-2023">writing about Rails World</a> 
on returning from Amsterdam as it was simply spectacular. Fast forward to today as I sit in my hotel 
room about to head home from <a href="https://www.youtube.com/watch?v=PzsRWH0n6O4">Rails World 2024</a> in Toronto, the feeling is only amplified tenfold. 
This extraordinary bringing together of the Rails community to celebrate, share and learn is something 
truly special.</p>

<p><img src="/assets/images/articles/rails-8-sign-rw24.jpg" alt="Rails 8 - Rails World 2024 Toronto" /></p>

<h2 id="highlights">Highlights</h2>

<p>The announcements in <a href="https://www.youtube.com/watch?v=-cEn_83zRFw">DHH’s keynote</a> and release of Rails 8 were 
obvious highlights, but the energy and excitement that seems to be only getting more amplified since last year
is what left the biggest impression.</p>

<p>The singular highlight was the moment David presented Matz with the Rails lifetime achievement award, and the 
sheer surprise and joy in Matz’ face that I managed to capture in this photo and <a href="https://x.com/tapster/status/1839429924819808696">tweet</a> from the front row! (noticed DHH used this in his <a href="https://world.hey.com/dhh/wonderful-rails-world-vibes-7a6141d2">blog post</a> too :D)</p>

<p><img src="/assets/images/articles/dhh-matz-rw24.jpg" alt="DHH presenting Lifetime Award to Matz" /></p>

<p>All of the sessions will be released on the official <a href="https://www.youtube.com/@railsofficial">Rails YouTube channel</a> 
over the next couple of weeks so make sure to check and enjoy any you missed or if you weren’t lucky enough to attend.</p>

<h2 id="rails-foundation">Rails Foundation</h2>

<p>Lastly, I must thank <a href="https://cookpad.com/uk">Cookpad</a> for supporting me and - with the other founding members - for their continued support of the Rails Foundation, without which none of this would happen.</p>

<p><img src="/assets/images/articles/foundation-team-rw24.png" alt="Rails Foundation Members at Rails World 2024 in Toronto" /></p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Rails World 2024]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/rails-8-sign-rw24.jpg" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/rails-8-sign-rw24.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Action Text Video Support</title><link href="https://mileswoodroffe.com/articles/action-text-video-support" rel="alternate" type="text/html" title="Action Text Video Support" /><published>2024-08-19T00:00:00+00:00</published><updated>2024-08-19T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/action-text-video-support</id><content type="html" xml:base="https://mileswoodroffe.com/articles/action-text-video-support"><![CDATA[<h2 id="action-text">Action Text</h2>

<p>Here’s a quick post just to document how I got video uploads working in Action Text, again mostly for “future me”, 
but hopefully helpful for others!</p>

<p>Let me say, I love Action Text and Trix. For me at least its exactly what I need for simple text input that users can format
and also upload images. I won’t go into the details of how to set it up as there are countless great sources for that, 
the primary one of course being the <a href="https://guides.rubyonrails.org/action_text_overview.html">Rails Guides</a> which are great.</p>

<h2 id="file-uploads">File uploads</h2>

<p>One of the killer features of Action Text in my opinion is the way it handles file uploads so simply with a WYSIWYG interface. 
This is great for images but I couldn’t figure out why I couldn’t play uploaded <code class="language-plaintext highlighter-rouge">.mov</code> video files. And I really needed 
this for my use case of sharing technical issues and bug reports.</p>

<h2 id="default-templates">Default Templates</h2>

<p>When you install Action Text with <code class="language-plaintext highlighter-rouge">bin/rails action_text:install</code> the default template for representing attachments will be 
generated in <code class="language-plaintext highlighter-rouge">app/views/active_storage/blobs/_blob.html.erb</code> and looks like this (Rails 8.0.0.alpha)</p>
<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;figure</span> <span class="na">class=</span><span class="s">"attachment attachment--</span><span class="cp">&lt;%=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">representable?</span> <span class="p">?</span> <span class="s2">"preview"</span> <span class="p">:</span> <span class="s2">"file"</span> <span class="cp">%&gt;</span><span class="s"> attachment--</span><span class="cp">&lt;%=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">filename</span><span class="p">.</span><span class="nf">extension</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">blob</span><span class="p">.</span><span class="nf">representable?</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">image_tag</span> <span class="n">blob</span><span class="p">.</span><span class="nf">representation</span><span class="p">(</span><span class="ss">resize_to_limit: </span><span class="n">local_assigns</span><span class="p">[</span><span class="ss">:in_gallery</span><span class="p">]</span> <span class="p">?</span> <span class="p">[</span> <span class="mi">800</span><span class="p">,</span> <span class="mi">600</span> <span class="p">]</span> <span class="p">:</span> <span class="p">[</span> <span class="mi">1024</span><span class="p">,</span> <span class="mi">768</span> <span class="p">])</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>

  <span class="nt">&lt;figcaption</span> <span class="na">class=</span><span class="s">"attachment__caption"</span><span class="nt">&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">caption</span> <span class="o">=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">try</span><span class="p">(</span><span class="ss">:caption</span><span class="p">)</span> <span class="cp">%&gt;</span>
      <span class="cp">&lt;%=</span> <span class="n">caption</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">else</span> <span class="cp">%&gt;</span>
      <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"attachment__name"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">blob</span><span class="p">.</span><span class="nf">filename</span> <span class="cp">%&gt;</span><span class="nt">&lt;/span&gt;</span>
      <span class="nt">&lt;span</span> <span class="na">class=</span><span class="s">"attachment__size"</span><span class="nt">&gt;</span><span class="cp">&lt;%=</span> <span class="n">number_to_human_size</span> <span class="n">blob</span><span class="p">.</span><span class="nf">byte_size</span> <span class="cp">%&gt;</span><span class="nt">&lt;/span&gt;</span>
    <span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
  <span class="nt">&lt;/figcaption&gt;</span>
<span class="nt">&lt;/figure&gt;</span>
</code></pre></div></div>

<p>I found that a thumbnail of the video was generated and displayed when I rendered out the content but I wanted to play the video! 
I expected adding something like this using the <code class="language-plaintext highlighter-rouge">AssetTagHelper</code> for video would render accordingly:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%</span> <span class="k">if</span> <span class="n">blob</span><span class="p">.</span><span class="nf">video?</span> <span class="cp">%&gt;</span>
    <span class="cp">&lt;%=</span> <span class="n">video_tag</span> <span class="n">url_for</span><span class="p">(</span><span class="n">blob</span><span class="p">),</span> <span class="ss">controls: </span><span class="kp">true</span><span class="p">,</span> <span class="ss">preload: </span><span class="s2">"metadata"</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"attachment__video"</span> <span class="cp">%&gt;</span>
...
</code></pre></div></div>
<p>but while I got a great preview still image of the video, I couldn’t play it…</p>

<h2 id="sanitization">Sanitization</h2>

<p>Finally after a lot of head scratching I discovered that the tags were being sanitized away. Sanitizing the form input is critical 
since users may try all kinds of nefarious exploits to compromise our sites, but in this case supporting video seemed to be an ok 
trade-off.</p>

<p>The trick then is to allow the video related tags in the HTML to be rendered when showing Action Text content. To do this we need to 
add the following initializer in <code class="language-plaintext highlighter-rouge">config/initializers/action_text.rb</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">config</span><span class="p">.</span><span class="nf">after_initialize</span> <span class="k">do</span>
  <span class="n">default_allowed_attributes</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">::</span><span class="no">HTML5</span><span class="o">::</span><span class="no">Sanitizer</span><span class="p">.</span><span class="nf">safe_list_sanitizer</span><span class="p">.</span><span class="nf">allowed_attributes</span> <span class="o">+</span> <span class="no">ActionText</span><span class="o">::</span><span class="no">Attachment</span><span class="o">::</span><span class="no">ATTRIBUTES</span><span class="p">.</span><span class="nf">to_set</span>
  <span class="n">custom_allowed_attributes</span> <span class="o">=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="sx">%w[controls]</span><span class="p">)</span>
  <span class="no">ActionText</span><span class="o">::</span><span class="no">ContentHelper</span><span class="p">.</span><span class="nf">allowed_attributes</span> <span class="o">=</span> <span class="p">(</span><span class="n">default_allowed_attributes</span> <span class="o">+</span> <span class="n">custom_allowed_attributes</span><span class="p">).</span><span class="nf">freeze</span>

  <span class="n">default_allowed_tags</span> <span class="o">=</span> <span class="no">Rails</span><span class="o">::</span><span class="no">HTML5</span><span class="o">::</span><span class="no">Sanitizer</span><span class="p">.</span><span class="nf">safe_list_sanitizer</span><span class="p">.</span><span class="nf">allowed_tags</span> <span class="o">+</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span> <span class="no">ActionText</span><span class="o">::</span><span class="no">Attachment</span><span class="p">.</span><span class="nf">tag_name</span><span class="p">,</span> <span class="s2">"figure"</span><span class="p">,</span> <span class="s2">"figcaption"</span> <span class="p">])</span>
  <span class="n">custom_allowed_tags</span> <span class="o">=</span> <span class="no">Set</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="sx">%w[audio video source]</span><span class="p">)</span>
  <span class="no">ActionText</span><span class="o">::</span><span class="no">ContentHelper</span><span class="p">.</span><span class="nf">allowed_tags</span> <span class="o">=</span> <span class="p">(</span><span class="n">default_allowed_tags</span> <span class="o">+</span> <span class="n">custom_allowed_tags</span><span class="p">).</span><span class="nf">freeze</span>
<span class="k">end</span>
</code></pre></div></div>

<p>What this is doing is telling Action Text to allow a “controls” HTML attribute and “audio”, “video” and “source” tags to be rendered from user 
uploaded content.</p>

<p>After restarting the server.. voila! You should see a video play button and scrub control!</p>

<h2 id="wrap-up">Wrap Up</h2>

<p>As always, don’t hesitate to drop me a note via <a href="https://twitter.com/tapster">twitter</a> or any other channels listed <a href="/about">here</a>.</p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Action Text]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Rails is 20</title><link href="https://mileswoodroffe.com/articles/rails-is-20" rel="alternate" type="text/html" title="Rails is 20" /><published>2024-08-05T00:00:00+00:00</published><updated>2024-08-05T00:00:00+00:00</updated><id>https://mileswoodroffe.com/articles/rails-is-20</id><content type="html" xml:base="https://mileswoodroffe.com/articles/rails-is-20"><![CDATA[<h2 id="happy-birthday-rails">Happy Birthday Rails!</h2>

<p>A quick note and recognition what a huge milestone just came with the 20th anniversary of the launch of Ruby on Rails,
which was originally <a href="https://web.archive.org/web/20040823214652/http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/107370">announced on July 25th, 2004</a>!</p>

<p>A truly groundbreaking framework that is behind so many of the service we know and love over the last 20 years, it made me 
reflect on my journey and how grateful I am I found Rails.</p>

<h2 id="my-rails-story">My Rails Story</h2>

<p>Back then I was living in San Francisco, having hung up the headphones on my touring sound engineer days, and was 4 years
into turning my hobby in to work as a software developer. I was working in the ringtone business by then - remember those? -
and our tech stack was Java, JSP, Servlets. You get the picture.</p>

<p>It wasn’t until late 2005 when I saw the seminal <a href="https://www.youtube.com/watch?v=Gzj723LkRJY">“How to Build a Blog Engine in 15 Minutes with Ruby on Rails”</a>
video from <a href="https://dhh.dk/">dhh</a> when I first heard of Rails. I was immediately intrigued enough to build the blog from
the demo, then start putting together some very simple reporting interfaces and tools for work. It really was
groundbreaking and fun.</p>

<p>My office at the time was in the “Organic” building at 3rd &amp; Bryant, directly over the road from South Park where another
little Ruby on Rails project was getting started, called “Twitter”! or “Twttr” as it was. My memory is a bit fuzzy but 
I’m certain there were Ruby meetups at the Odeo/Twitter office, and definitely at CNET where a young chap called Chris 
Wandstrath was often talking about one new plugin or another like cache_fu. I wonder what ever happened to him? ;)</p>

<p>I went on to work at a few more startups in San Francisco over the next few years, always with Rails, before moving
to Japan in 2010 to join Cookpad, who were famously one of the biggest Ruby on Rails services at the time. Exciting!</p>

<h2 id="community">Community</h2>

<p>Ruby and Rails has always seemed to have an incredible community, and it’s another thing I am immensely grateful for. I’ve 
had the chance to meet and work with some incredible folks, and with massive thanks to Cookpad, was able to lead the charge
in supporting and attending numerous events and conferences worldwide. Again. Thanks Rails!</p>

<h2 id="ruby">Ruby</h2>

<p>Rails also introduced me to Ruby, like so many others, and I’ve been fortunate to be working with this incredible match up
of expressive language and impressive framework ever since for the last 18 years! After moving back to the UK with Cookpad 
I was able to invite Matz, Koichi and Endoh-san to Bristol where somehow I manage to pull off an <a href="https://sourcediving.com/hacking-ruby-with-matz-koichi-aaron-and-mame-26abd7a0aa15">event that is almost 
unimaginable</a> with Raphael, Tenderlove, 
and a whole host of open source Ruby and Rails developers.</p>

<h2 id="the-rail-foundation">The Rail Foundation</h2>

<p>Lastly, when DHH contacted me in 2021 to pitch his idea of a Foundation to support the future success of Rails I immediately
wanted to help. Fortunately through the incredible generosity of the leadership, Cookpad became one of the founding members of 
the Rails Foundation, and I was nominated to sit on the board. Through the foundation we’ve been able to launch an annual 
conference which got off to a <a href="/articles/rails-world-2023">spectacular start last year in Amsterdam</a> and will be back in 
<a href="https://rubyonrails.org/world/2024">Toronto next month</a> as well as numerous programs to improve documentation, tutorials and 
provide opportunities for people new to the framework.</p>

<h2 id="thanks-dhh-the-core-team-and-all-contributors">Thanks DHH, the Core Team, and all contributors</h2>

<p>I owe so much of my career and experiences to the joy I’ve had from working with Ruby, working with Rails, working with Ruby
people. I can’t thank all those involved enough. What a ride. And here’s to the next 20!</p>

<h2 id="wrap-up">Wrap Up</h2>

<p>As always, don’t hesitate to drop me a note via <a href="https://twitter.com/tapster">twitter</a> or any other channels listed <a href="/about">here</a>.</p>]]></content><author><name>Miles Woodroffe</name><email>miles.woodroffe@gmail.com</email></author><category term="tech" /><category term="rails" /><summary type="html"><![CDATA[Happy Birthday Rails!]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" /><media:content medium="image" url="https://mileswoodroffe.com/assets/images/articles/random-rails.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>