<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
    <channel>
        <title>Kyle Keesling</title>
        <link>https://kylekeesling.dev</link>
        <description>
            an Indianapolis-based Rubyist &amp; Full-stack Developer
        </description>
        
            <item>
                
                    <title>Migrating from Sidekiq to Solid Queue</title>
                    <link>https://kylekeesling.dev/posts/2024/01/migrating-from-sidekiq-to-solid-queue</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2024/01/migrating-from-sidekiq-to-solid-queue</guid>
                

                <pubDate>Wed, 03 Jan 2024 11:00:00 -0500</pubDate>
                <description>
                    &lt;p&gt;With the recent release of &lt;a href=&quot;https://github.com/basecamp/solid_queue&quot;&gt;Solid Queue&lt;/a&gt;, and a little bit
of extra free time due to the holidays, I made a decision to migrate my apps away from
&lt;a href=&quot;https://github.com/sidekiq/sidekiq&quot;&gt;Sidekiq&lt;/a&gt;. The idea of simplifying my infrastructure requirements
by removing Redis from the equation, and the relatively vanilla requirements I have for a background
queuing system were just enough to convince me this was a worthwhile exercise.&lt;/p&gt;

&lt;p&gt;I won’t bore you with every machinations of copying, pasting, and deleting code since it’s realtively
straightforward, but I did think it was worth sharing a few highlights, considerations, and
gotchas I came across.&lt;/p&gt;

&lt;h2 id=&quot;the-migration&quot;&gt;The Migration&lt;/h2&gt;

&lt;p&gt;The switchover in my case was very straightforward since my use of Sidekiq was all via
&lt;a href=&quot;https://guides.rubyonrails.org/active_job_basics.html&quot;&gt;Active Job&lt;/a&gt;. It consisted of:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Swapping out the gems in my &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Running &lt;a href=&quot;https://github.com/basecamp/solid_queue#installation-and-usage&quot;&gt;the Solid Queue installation scripts&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;Removing the Sidekiq UI mounting lines from &lt;code class=&quot;highlighter-rouge&quot;&gt;routes.rb&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Deleting &lt;code class=&quot;highlighter-rouge&quot;&gt;sidekiq.rb&lt;/code&gt; from &lt;code class=&quot;highlighter-rouge&quot;&gt;config/initializers&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Tweaking my &lt;a href=&quot;https://github.com/basecamp/solid_queue#configuration&quot;&gt;Solid Queue config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;configuration&quot;&gt;Configuration&lt;/h2&gt;
&lt;p&gt;I migrated two apps to Solid Queue. One of them mainly uses ActiveJob to send emails and run reports,
so I simply replied on the default config, but my other app does utilize multiple queues, so I
ended up with the following config for it:&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# config/solid_queue.yml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;default&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nl&quot;&gt;&amp;amp;default&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;workers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;queues&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;*&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;polling_interval&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;queues&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;real_time&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;polling_interval&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0.1&lt;/span&gt;

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

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

&lt;span class=&quot;na&quot;&gt;production&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;*default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;My goal here was to create a priorty queue, called &lt;code class=&quot;highlighter-rouge&quot;&gt;real_time&lt;/code&gt;, so I decided to add a second worker
dedicated soley to those jobs, and to poll for them at a much higher frequency.&lt;/p&gt;

&lt;h2 id=&quot;a-few-considerations&quot;&gt;A Few Considerations&lt;/h2&gt;
&lt;p&gt;My app with the completely vanilla config deployed and worked without a hitch, but I did run into a
few hiccups with my custom config.&lt;/p&gt;

&lt;h3 id=&quot;db-connection-pool&quot;&gt;DB Connection Pool&lt;/h3&gt;
&lt;p&gt;I deploy my apps on Heroku, and by default the app is setup to look at the &lt;code class=&quot;highlighter-rouge&quot;&gt;DB_POOL&lt;/code&gt; environmental
variable to set the connection pool in &lt;code class=&quot;highlighter-rouge&quot;&gt;config/database.yml&lt;/code&gt;. This is fine for app instances but
since I’m running a dedicated worker dyno for Solid Queue, some extra work was needed.&lt;/p&gt;

&lt;p&gt;By default each Solid Queue worker has 5 threads, meaning that since my app has two workers, I
needed a connection pool that allowed 10 connections. I was able to accomplish this by using a specfic
enviroment variable, &lt;code class=&quot;highlighter-rouge&quot;&gt;SOLID_QUEUE_DB_POOL&lt;/code&gt;, as is suggested &lt;a href=&quot;https://devcenter.heroku.com/articles/concurrency-and-database-connections#background-workers&quot;&gt;in the Heroku docs&lt;/a&gt;.&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Procfile&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bundle exec puma -C config/puma.rb&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;worker&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DB_POOL=$SOLID_QUEUE_DB_POOL bundle exec rake solid_queue:start&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;release&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;bundle exec rails db:migrate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;error-handling&quot;&gt;Error Handling&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note&lt;/strong&gt;: A previous version of this post referred to the &lt;code class=&quot;highlighter-rouge&quot;&gt;on_thread_error&lt;/code&gt; setting to catch job errors. &lt;a href=&quot;https://github.com/basecamp/solid_queue/issues/120#issuecomment-1894413948&quot;&gt;After some more
digging&lt;/a&gt; I realized this
setting is not for job errors but rather actual errors that occur that are related to the thread directly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By default the library silently handles errors and marks them as failed, but I prefer being proactively
notified when something happens. To do this you’ll need to leverage an &lt;code class=&quot;highlighter-rouge&quot;&gt;around_perform&lt;/code&gt; callback to wrap
your jobs in an error handler, then report them as necessary&lt;/p&gt;

&lt;div class=&quot;language-ruby highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ApplicationJob&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveJob&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Base&lt;/span&gt;
  &lt;span class=&quot;n&quot;&gt;around_perform&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;capture_and_record_errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;capture_and_record_errors&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;block&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;call&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# I had to use rescue here instead of a `Rails.error` block because Honeybadger ignores the `Rails.error.report` call&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# in favor of their own error handler, which is fine in most cases, but unfortunately doesn&apos;t work here. Report would be&lt;/span&gt;
  &lt;span class=&quot;c1&quot;&gt;# great here because it re-raises the error, but instead I have to do that manually&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;rescue&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Exception&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;set_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;**&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;error_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;report&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;raise&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;e&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;error_context&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;active_job: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;arguments: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;arguments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;scheduled_at: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;scheduled_at&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;job_id: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;job&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;job_id&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A huge thanks goes out to &lt;a href=&quot;https://github.com/rosa&quot;&gt;Rosa Gutierrez&lt;/a&gt; for not only her wonderful work on this gem, but also for
taking the time to respond to Github issues.&lt;/p&gt;

&lt;h3 id=&quot;failures-and-retries&quot;&gt;Failures and Retries&lt;/h3&gt;
&lt;p&gt;By default, &lt;a href=&quot;https://github.com/sidekiq/sidekiq/wiki/Error-Handling&quot;&gt;Sidekiq has a built-in retry mechanism&lt;/a&gt;,
but &lt;a href=&quot;https://github.com/basecamp/solid_queue#failed-jobs-and-retries&quot;&gt;this is not the case for Solid Queue&lt;/a&gt;.
If automatic retrying is important for you you’ll need configure this on a per-job basis, or in
&lt;code class=&quot;highlighter-rouge&quot;&gt;application_job.rb&lt;/code&gt;, via &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_job_basics.html#retrying-or-discarding-failed-jobs&quot;&gt;Active Job’s &lt;code class=&quot;highlighter-rouge&quot;&gt;retry_on&lt;/code&gt; API&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;wrapping-up&quot;&gt;Wrapping Up&lt;/h2&gt;
&lt;p&gt;So far I’m feeling good about the simplification that this has afforded me/my apps, but I do find myself
missing the UI that Sidekiq provided, but &lt;a href=&quot;https://github.com/basecamp/solid_queue/issues/70&quot;&gt;it does not seem that we’ll have to wait too long for an
answer for that either&lt;/a&gt;.&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Deploying a Rails App Using Kamal and SQLite</title>
                    <link>https://kylekeesling.dev/posts/2023/10/deploying-a-rails-app-using-kamal-and-sqlite</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2023/10/deploying-a-rails-app-using-kamal-and-sqlite</guid>
                

                <pubDate>Fri, 13 Oct 2023 00:00:00 -0400</pubDate>
                <description>
                    &lt;p&gt;There’s been a lot of excitement lately in the Rails community. Rails World just wrapped up, which
brought with it some really great annoucements for the future of the platform, as well as the release
of &lt;a href=&quot;https://rubyonrails.org/2023/10/5/Rails-7-1-0-has-been-released&quot;&gt;Rails 7.1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This most recent release had a few particular additions/improvements that really piqued my interest
in regards to how I approach deployment of new Rails apps, specifically around the inclusion of a
&lt;a href=&quot;https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt&quot;&gt;standard Dockerfile&lt;/a&gt; as well as a focus on making &lt;a href=&quot;https://www.sqlite.org/index.html&quot;&gt;SQLite&lt;/a&gt; a
first-class citizen/consideration for production apps.&lt;/p&gt;

&lt;p&gt;When you combine that with my recent discovery of the &lt;a href=&quot;https://github.com/oldmoe/litestack&quot;&gt;litestack gem&lt;/a&gt;,
the recent release of &lt;a href=&quot;https://kamal-deploy.org/&quot;&gt;Kamal&lt;/a&gt;,
and the &lt;a href=&quot;https://gorails.com/series/build-a-url-shortener-with-rails-7&quot;&gt;recent GoRails series&lt;/a&gt;
on building your own URL shortner, and the stars had aligned.&lt;/p&gt;

&lt;h2 id=&quot;prerequisites&quot;&gt;Prerequisites&lt;/h2&gt;
&lt;p&gt;Before we continue, here are a few things you’ll want to make sure you’ve got setup/configured properly:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Your app is using Rails 7.1 and has the default Dockerfile available to it&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://www.docker.com/get-started/&quot;&gt;Docker&lt;/a&gt; is installed on your local computer&lt;/li&gt;
  &lt;li&gt;You have an account with a hosting provider like &lt;a href=&quot;https://www.digitalocean.com/&quot;&gt;DigitalOcean&lt;/a&gt; or
&lt;a href=&quot;https://www.hetzner.com/&quot;&gt;Hetzner&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;sqlite--litestack&quot;&gt;SQLite + Litestack&lt;/h2&gt;

&lt;p&gt;For this project I wanted to host my project on the cheapest DigitalOcean droplet possible,
but it included some advanced requirements/features, including ActiveJob and ActionCable for background tasks and Turbo Broadcasts. While this certainly could have been done in the more traditional way of installing a
traditional database like PostgreSQL or MySQL and redis on the server, my goal was simplicity.&lt;/p&gt;

&lt;p&gt;In contrast, leveraging a single technology like SQLite for all of these core aspects of the app would
allow me to greatly simplify the hosting requirements, which is where Litestack comes in.&lt;/p&gt;

&lt;p&gt;If you aren’t familiar, &lt;a href=&quot;https://github.com/oldmoe/litestack&quot;&gt;Litestack&lt;/a&gt; is a wonderful project with the following objective:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Litestack is a Ruby gem that provides both Ruby and Ruby on Rails applications an all-in-one solution for web application data infrastructure. It exploits the power and embeddedness of SQLite to deliver a full-fledged SQL database, a fast cache , a robust job queue, a reliable message broker, a full text search engine and a metrics platform all in a single package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Installing the gem is very straightforward, drop it in your &lt;code class=&quot;highlighter-rouge&quot;&gt;Gemfile&lt;/code&gt; and run the installation command
&lt;code class=&quot;highlighter-rouge&quot;&gt;rails generate litestack:install&lt;/code&gt;.&lt;/p&gt;

&lt;h2 id=&quot;kamal&quot;&gt;Kamal&lt;/h2&gt;

&lt;p&gt;I’ll admit it, I’m late to the Docker party, so there were many of the concepts I was (and am still)
unfamiliar with, but the abstraction Kamal provides and the default Rails &lt;code class=&quot;highlighter-rouge&quot;&gt;Dockerfile&lt;/code&gt; are &lt;em&gt;just&lt;/em&gt;
enough to allow me to get the job done.&lt;/p&gt;

&lt;p&gt;That being said I do think there’s value in more broadly understanding Docker as well as things
like Traefik, which is something that’s mentioned many times in Kamal’s docs, but is never really
explained. I can piece together that it’s an orchestration and monitoring layer for your Docker
containers, but I’ve found it difficult to learn much more about it and how to use it, so if you
know of any good resources on better understanding it, please share.&lt;/p&gt;

&lt;p&gt;The main gotcha I ran into was around how to store the SQLite databases in persistant storage.
I solved this by configuring a &lt;a href=&quot;https://kamal-deploy.org/docs/configuration#using-volumes&quot;&gt;Kamal volume&lt;/a&gt;
and adding the following environmental variable to my &lt;code class=&quot;highlighter-rouge&quot;&gt;.env&lt;/code&gt; file: &lt;code class=&quot;highlighter-rouge&quot;&gt;LITESTACK_DATA_PATH=./storage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After a decent amount of trial and error, and losing my database between deploys more than once 😆,
the following deployment config is what ended up getting the job done.&lt;/p&gt;

&lt;div class=&quot;language-yml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# deploy.yml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;service&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;url-shortner&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kylekeesling/url-shortner&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;storage:/rails/storage&quot;&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;servers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;web&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;hosts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;192.241.138.163&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;add-host&quot;&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;host.docker.internal:host-gateway&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.rails_recipes.entrypoints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;websecure&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.rails_recipes.rule&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Host(`links.passtesting.com`)&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;traefik.http.routers.rails_recipes.tls.certresolver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;letsencrypt&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;registry&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;username&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kylekeesling&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;KAMAL_REGISTRY_PASSWORD&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;secret&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;RAILS_MASTER_KEY&lt;/span&gt;
    &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;LITESTACK_DATA_PATH&lt;/span&gt;

&lt;span class=&quot;na&quot;&gt;traefik&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;publish&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;443:443&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;volume&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/letsencrypt/acme.json:/letsencrypt/acme.json&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.address&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;:80&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.websecure.address&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;:443&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entryPoint.to&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;websecure&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entryPoint.scheme&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;https&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entryPoints.web.http.redirections.entrypoint.permanent&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;entrypoints.websecure.http.tls&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;entrypoints.websecure.http.tls.domains[0].main&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;links.passtesting.com&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.email&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;kyle.keesling@passtesting.com&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.storage&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;/letsencrypt/acme.json&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.httpchallenge&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;certificatesResolvers.letsencrypt.acme.httpchallenge.entrypoint&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;web&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;credit-and-thanks&quot;&gt;Credit and Thanks&lt;/h2&gt;

&lt;p&gt;I’d like to send out a huge thanks to &lt;a href=&quot;https://greg.molnar.io/blog/deploying-a-rails-app-with-kamal/&quot;&gt;Greg Molnar&lt;/a&gt; and &lt;a href=&quot;https://www.erikminkel.com/2023/09/29/using-kamal-to-host-multiple-apps-on-a-single-server/&quot;&gt;Erik Minkel&lt;/a&gt;,
who both provided some great content to help me along the way.&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Testing Stripe Webhooks with Minitest</title>
                    <link>https://kylekeesling.dev/posts/2023/09/testing-stripe-webhooks-with-minitest</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2023/09/testing-stripe-webhooks-with-minitest</guid>
                

                <pubDate>Fri, 29 Sep 2023 12:00:00 -0400</pubDate>
                <description>
                    &lt;p&gt;As I was looking to sure up some billing code in one of my projects, I figured it was finally time that
I created some tests for the various &lt;a href=&quot;https://stripe.com/docs/webhooks&quot;&gt;Stripe webhooks&lt;/a&gt; that we used.&lt;/p&gt;

&lt;p&gt;I was a bit dishearted that the only webhook testing examples I could find online pertained to
RSpec, so with that I rolled up my sleaves and dug into the internals of both the &lt;code class=&quot;highlighter-rouge&quot;&gt;stripe_event&lt;/code&gt; gem
as well as &lt;a href=&quot;https://github.com/stripe/stripe-ruby&quot;&gt;Stripe’s offical gem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After a few rounds of trial and error I was able to discern how Stripe generates the signature
for webhooks and how I could mimic it to test my endpoint. You can see that below in the
&lt;code class=&quot;highlighter-rouge&quot;&gt;webhook_header&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;You’ll also need to grab sample payloads for each of the webhook events you’d like to test and save
them as a JSON file in &lt;code class=&quot;highlighter-rouge&quot;&gt;test/fixtures/stripe/#{event}.json&lt;/code&gt;, as seen in the &lt;code class=&quot;highlighter-rouge&quot;&gt;webhook_payload&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;If you aren’t sure where to grab sample payloads from,
the &lt;a href=&quot;https://stripe.com/docs/stripe-cli&quot;&gt;Stripe CLI&lt;/a&gt; has a &lt;code class=&quot;highlighter-rouge&quot;&gt;trigger&lt;/code&gt; command that can make
gathering them much easier.&lt;/p&gt;

&lt;p&gt;The only other varaible you may have is the path to your webhook endpoint, which in my
case is &lt;code class=&quot;highlighter-rouge&quot;&gt;stripe_event_path&lt;/code&gt;, as seen in the &lt;code class=&quot;highlighter-rouge&quot;&gt;deliver_payload&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Take all of that and you get the following test case can be used as a base class for any
tests you may need to write:&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# test/stripe_test_case.rb&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;test_helper&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;StripeWebhookTestCase&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActionDispatch&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;IntegrationTest&lt;/span&gt;
  &lt;span class=&quot;no&quot;&gt;SIGNING_SECRET&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ENV&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;STRIPE_SIGNING_SECRET&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;deliver_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;headers&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;webhook_setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stripe_event_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;headers:
  &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;webhook_setup&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;webhook_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;webhook_header&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)]&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;webhook_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;no&quot;&gt;File&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;read&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Rails&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;root&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;test&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;fixtures&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;stripe&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;event&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.json&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;webhook_header&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;time&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Stripe&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Webhook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;compute_signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;SIGNING_SECRET&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;header&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;Stripe&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Webhook&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;generate_header&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;signature&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Stripe-Signature&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;header&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Once you have this test case, here’s an example test that could be used:&lt;/p&gt;

&lt;div class=&quot;language-rb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# test/webhooks/stripe/general_webhook_test.rb&lt;/span&gt;
&lt;span class=&quot;nb&quot;&gt;require&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;stripe_webhook_test_case&quot;&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;Stripe::GeneralWebhookTest&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;StripeWebhookTestCase&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webhook endpoint returns proper status code&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;deliver_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;event: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;charge.succeeded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;assert_response&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:ok&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webhook endpoint returns proper status code when no headers are provided&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stripe_event_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;params: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webhook_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;charge.succeeded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;assert_response&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:bad_request&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

  &lt;span class=&quot;nb&quot;&gt;test&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;webhook endpoint returns proper status code when improper headers are provided&quot;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;post&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stripe_event_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;params: &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;webhook_payload&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;charge.succeeded&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
      &lt;span class=&quot;ss&quot;&gt;headers: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Stripe-Signature&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;#{&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Time&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;now&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;to_i&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;,v1=badsignature&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;n&quot;&gt;assert_response&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:bad_request&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;In this project I also happen to be using the &lt;a href=&quot;https://github.com/integrallis/stripe_event&quot;&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;stripe_event&lt;/code&gt; gem&lt;/a&gt;,
so a good deal of the actual route/endpoint logic is abstracted from my app. I’m currently considering rolling
my own completely and removing this gem as a dependancy, as I’ve done in another project already, but
that’s a topic for another post. The Stripe docs also have
&lt;a href=&quot;https://stripe.com/docs/webhooks#example-endpoint&quot;&gt;a really good example&lt;/a&gt; one could use as a starting point.&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Add Stripe Checkout to your Rails app in Under 10 Minutes</title>
                    <link>https://kylekeesling.dev/posts/2022/01/stripe-checkout-in-rails-in-under-10-minutes</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2022/01/stripe-checkout-in-rails-in-under-10-minutes</guid>
                

                <pubDate>Mon, 24 Jan 2022 13:45:00 -0500</pubDate>
                <description>
                    &lt;div style=&quot;text-align: center;&quot;&gt;
&lt;iframe width=&quot;560&quot; height=&quot;315&quot; class=&quot;mt-5&quot; style=&quot;margin: 5px auto;&quot; src=&quot;https://www.youtube.com/embed/ji_rNMTaF9g&quot; title=&quot;Add Stripe Checkout to your Rails app in Under 10 Minutes&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen=&quot;&quot;&gt;
&lt;/iframe&gt;
&lt;/div&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Migrating to jsbundling-rails</title>
                    <link>https://kylekeesling.dev/posts/2022/01/jsbundling-rails</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2022/01/jsbundling-rails</guid>
                

                <pubDate>Sun, 23 Jan 2022 11:00:00 -0500</pubDate>
                <description>
                    &lt;p&gt;Now that &lt;a href=&quot;https://github.com/rails/webpacker/commit/16bba5d6ca862b950a002f19c10d12e4bf51a87b#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5&quot;&gt;Webpacker is riding off into the sunset&lt;/a&gt;, many of us find ourselves switching over to
using one of the solutions offered by &lt;a href=&quot;https://github.com/rails/jsbundling-rails&quot;&gt;jsbuilding-rails&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;With the move comes a much simplier javascript story, which is a welcome change for many of us,
but making the move can quickly lead to a few gotchas. Below I’ll outline the few that I’ve run into
and how you can avoid them so you can ensure a smooth transition to using esbuild.&lt;/p&gt;

&lt;h2 id=&quot;gotcha-1---importing-css-in-your-javascript&quot;&gt;Gotcha #1 - Importing CSS in your Javascript&lt;/h2&gt;

&lt;p&gt;This has been one of the most common problems I’ve noticed folks having. Say you’re using a javascript
package that also includes CSS assets, esbuild does allow you to import those, but will name
the output file the same name as the root javascript file in &lt;code class=&quot;highlighter-rouge&quot;&gt;app/javascript&lt;/code&gt; which in most cases will be &lt;code class=&quot;highlighter-rouge&quot;&gt;application.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The result of that being two files output to &lt;code class=&quot;highlighter-rouge&quot;&gt;app/assets/build&lt;/code&gt; , &lt;code class=&quot;highlighter-rouge&quot;&gt;application.js&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;application.css&lt;/code&gt; - and that’s where the hang-up occurs. If you are also using &lt;a href=&quot;https://github.com/rails/cssbundling-rails&quot;&gt;cssbundling-rails&lt;/a&gt; it will by default name it’s output the same thing - &lt;code class=&quot;highlighter-rouge&quot;&gt;application.css&lt;/code&gt;, causing a naming collision that will inevitably cause you to bang your head against the wall if you aren’t sure what’s going on.&lt;/p&gt;

&lt;p&gt;There are two solutions here, one is renaming the output of your &lt;code class=&quot;highlighter-rouge&quot;&gt;cssbundling-rails&lt;/code&gt; file, or renaming your &lt;code class=&quot;highlighter-rouge&quot;&gt;app/javascript/application.js&lt;/code&gt; to something else, which in my case is what I did, making it &lt;code class=&quot;highlighter-rouge&quot;&gt;application-esbuild.js&lt;/code&gt;. Now, esbuild will process your javascript and CSS and place two files named &lt;code class=&quot;highlighter-rouge&quot;&gt;application-esbuild.js&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;application-esbuild.css&lt;/code&gt; into &lt;code class=&quot;highlighter-rouge&quot;&gt;app/assets/build&lt;/code&gt;. Then all you need to do is make sure you include those files in the head of your &lt;code class=&quot;highlighter-rouge&quot;&gt;application.html.erb&lt;/code&gt; file:&lt;/p&gt;

&lt;div class=&quot;language-erb highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;&amp;lt;!-- app/views/layout/application.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;media: :all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_track: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;reload&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;

&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application-esbuild&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;media: :all&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_track: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;reload&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;span class=&quot;cp&quot;&gt;&amp;lt;%=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;javascript_include_tag&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;application-esbuild&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;turbo_track: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;reload&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;cp&quot;&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;gotcha-2---having-two-javascript-directories&quot;&gt;Gotcha #2 - Having Two Javascript Directories&lt;/h2&gt;
&lt;p&gt;If you’re not starting fresh in Rails 7 there’s good chance you’ve got files in &lt;code class=&quot;highlighter-rouge&quot;&gt;app/assets/javascript&lt;/code&gt;.
If so you’ll also want to make sure you’ve got sprockets setup to serve those assets. This can be done in &lt;code class=&quot;highlighter-rouge&quot;&gt;app/assets/config/manifest.js&lt;/code&gt;, just make sure you’ve got the following line in the config:&lt;/p&gt;

&lt;div class=&quot;language-javascript highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// app/assets/config/manifest.js&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;//= link_tree ../images&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//= link_tree ../fonts&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//= link_tree ../builds&lt;/span&gt;

&lt;span class=&quot;c1&quot;&gt;// this is the key addition, referring to the file app/assets/javascript/application.js&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;//= link application.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;Of course you could also use &lt;code class=&quot;highlighter-rouge&quot;&gt;link_tree&lt;/code&gt; or any of the other methods that sprockets provides in order
to properly include the necessary javascript files in &lt;code class=&quot;highlighter-rouge&quot;&gt;app/assets/javascript/&lt;/code&gt;, but I think explicitly
including just one file helps keep things clean and easy to understand.&lt;/p&gt;

&lt;p&gt;Also note that doing this also assumes that you’ve renamed your esbuild output file to &lt;code class=&quot;highlighter-rouge&quot;&gt;application-esbuild.js&lt;/code&gt;
as mentioned above, otherwise you’d run into similar naming collision issues that Gotcha #1 covers.&lt;/p&gt;


                    
                </description>
            </item>
        
            <item>
                
                    <title>Now with Bridgetown</title>
                    <link>https://kylekeesling.dev/posts/2022/01/welcome-to-bridgetown</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2022/01/welcome-to-bridgetown</guid>
                

                <pubDate>Sun, 23 Jan 2022 08:57:33 -0500</pubDate>
                <description>
                    &lt;p&gt;After over 9 years of running this site on Jekyll I finally made the jump to &lt;a href=&quot;https://www.bridgetownrb.com&quot;&gt;Bridgetown&lt;/a&gt;. Shoutout to Jared and the team for making this migration fairly easy and straightforward.&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Setting Up Proper Amazon S3 Permissions for ActiveStorage</title>
                    <link>https://kylekeesling.dev/posts/2020/01/activestorage-s3-permissions</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2020/01/activestorage-s3-permissions</guid>
                

                <pubDate>Tue, 28 Jan 2020 12:43:00 -0500</pubDate>
                <description>
                    &lt;p&gt;If you’ve found yourself marveling at how cryptic and impenetrable understanding AWS services, then you are definitely not alone. For much too long have I relied on doing a quick web search and blindly copying and pasting settings and policies, so when I found myself doing it again this week for &lt;a href=&quot;https://passtesting.com/tools/service-providers&quot;&gt;one of my applications&lt;/a&gt; I decided it was time to slow down and actually understand what it was that I was doing.&lt;/p&gt;

&lt;h2 id=&quot;the-problem&quot;&gt;The Problem&lt;/h2&gt;

&lt;p&gt;I’m creating a demo environment that needs to be completely separate from our production data, so part of that entails creating its own S3 bucket for use with &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html&quot;&gt;ActiveStorage&lt;/a&gt;. Having not really done this since ActiveStorage was first released I begin DuckDuckGoing (is this even a thing?) for a how-to.&lt;/p&gt;

&lt;p&gt;Some perfectly fine write-ups and tutorials popped up on the first page, but I noticed that all of them recommended setting up an &lt;a href=&quot;https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users.html&quot;&gt;IAM User&lt;/a&gt; with &lt;code class=&quot;highlighter-rouge&quot;&gt;AmazonS3FullAccess&lt;/code&gt; permissions. As soon as I looked at the description for this policy it threw up a &lt;em&gt;big&lt;/em&gt; red flag:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;code class=&quot;highlighter-rouge&quot;&gt;AmazonS3FullAccess: Provides full access to all buckets via the AWS Management Console&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Full access to all buckets? That’s gonna be a big nope from me.&lt;/p&gt;

&lt;h2 id=&quot;the-solution&quot;&gt;The Solution&lt;/h2&gt;

&lt;p&gt;At an absolute minimum the permissions need to be locked down to a single bucket, but ideally we’d only give the app the ability to perform solely the actions that are required for the framework to function properly. Luckily there’s &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html#amazon-s3-service&quot;&gt;a callout right in the Rails guide&lt;/a&gt; that states:&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The core features of Active Storage require the following permissions: &lt;code class=&quot;highlighter-rouge&quot;&gt;s3:ListBucket&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;s3:PutObject&lt;/code&gt;, &lt;code class=&quot;highlighter-rouge&quot;&gt;s3:GetObject&lt;/code&gt;, and &lt;code class=&quot;highlighter-rouge&quot;&gt;s3:DeleteObject&lt;/code&gt;. If you have additional upload options configured such as setting ACLs then additional permissions may be required.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assuming you already have a bucket created, we just need to &lt;a href=&quot;https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html&quot;&gt;create an IAM User&lt;/a&gt; so we can generate the &lt;code class=&quot;highlighter-rouge&quot;&gt;access_key_id&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;secret_access_key&lt;/code&gt; necessary to configure the &lt;code class=&quot;highlighter-rouge&quot;&gt;config/storage.yml&lt;/code&gt; Amazon service.&lt;/p&gt;

&lt;p&gt;Once you have your IAM user created you can use the policy template I’ve built down below as a guide to either create and attach the policy to a group you add your IAM user to, or just attach the policy directly to the user, which is what I did.&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Version&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;2012-10-17&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Statement&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Sid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VisualEditor0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Effect&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3:PutObject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3:GetObject&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3:DeleteObject&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
          &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:s3:::BUCKET_NAME_GOES_HERE/*&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Sid&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;VisualEditor1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Effect&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Allow&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Action&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;s3:ListBucket&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;Resource&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;arn:aws:s3:::BUCKET_NAME_GOES_HERE&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;final-notes&quot;&gt;Final Notes&lt;/h2&gt;
&lt;p&gt;To their credit, one or two articles said something to the extent of “you may want to do this differently in production”, but let’s be honest, most people don’t read the entire article and/or will not remember to do this, so I find it to be a much better idea to set things up right from the start.&lt;/p&gt;

&lt;p&gt;Hopefully this helps you in your ActiveStorage-related endeavors, and if you have any suggestions or comments on how you handle S3 permissions, I’d love to hear them!&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Getting Honeybadger Uptime Alerts via SMS with Zapier</title>
                    <link>https://kylekeesling.dev/posts/2019/09/honeybadger-uptime-sms-alerts</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2019/09/honeybadger-uptime-sms-alerts</guid>
                

                <pubDate>Wed, 04 Sep 2019 05:00:00 -0400</pubDate>
                <description>
                    &lt;p&gt;&lt;em&gt;UPDATE: Shortly after posting this Honeybadger was &lt;a href=&quot;https://twitter.com/honeybadgerapp/status/1169684671556308992&quot;&gt;kind enough to let me know&lt;/a&gt;
that &lt;a href=&quot;https://docs.honeybadger.io/guides/profile.html#configuring-personal-alerts&quot;&gt;this is functionality that they offer directly&lt;/a&gt;&lt;/em&gt; 😁&lt;/p&gt;

&lt;p&gt;After using &lt;a href=&quot;https://www.honeybadger.io/&quot;&gt;Honeybadger&lt;/a&gt; Uptime Monitoring for a
few months, I found that these updates were sometimes getting lost in the
everyday chatter of my &lt;a href=&quot;/posts/2019/07/honeybadger-to-basecamp&quot;&gt;error messages that I have posted to a Basecamp chat&lt;/a&gt;
via Zapier.&lt;/p&gt;

&lt;p&gt;With that in mind I came up with the idea to create a new
&lt;a href=&quot;https://docs.honeybadger.io/guides/services.html#1-select-the-webhook-integration&quot;&gt;Honeybadger webhook alert&lt;/a&gt;
that would post only uptime updates to Zapier.&lt;/p&gt;

&lt;p&gt;Below is an overview of the Zap I came up with:&lt;/p&gt;

&lt;div class=&quot;text-center&quot;&gt;
&lt;img src=&quot;/images/2019/09/zapier-text-alert.png&quot; style=&quot;width: 30%;&quot; class=&quot;m-auto shadow-md&quot; /&gt;
&lt;/div&gt;

&lt;p&gt;With that now in place I now have Zapier text me whenever something big happens… which hopefully is rarely to never :)&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>Piping Your Honeybadger Events to Basecamp with Zapier</title>
                    <link>https://kylekeesling.dev/posts/2019/07/honeybadger-to-basecamp</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2019/07/honeybadger-to-basecamp</guid>
                

                <pubDate>Wed, 03 Jul 2019 09:30:00 -0400</pubDate>
                <description>
                    &lt;p&gt;My team has been working on consolidating and improving our communication tools
and practices, and as part of that we’ve decided to ween ourselves off of
&lt;a href=&quot;http://slack.com&quot;&gt;Slack&lt;/a&gt; and totally leverage &lt;a href=&quot;http://basecamp.com&quot;&gt;Basecamp&lt;/a&gt;
for all internal and customer communications.&lt;/p&gt;

&lt;p&gt;One of the major things that the team appriciated in Slack was the #dev channel I had created that
piped all of our deploys, pull requests, and errors into one central location,
allowing everyone to know what’s going on without having to tap someone on the shoulder.&lt;/p&gt;

&lt;p&gt;Basecamp provides an easy way, using a &lt;a href=&quot;https://github.com/basecamp/bc3-api/blob/master/sections/chatbots.md&quot;&gt;chatbot&lt;/a&gt;,
to feed Github events into a Campfire chat, but getting our application errors in
wasn’t quite as straightforward.&lt;/p&gt;

&lt;p&gt;We use &lt;a href=&quot;https://www.honeybadger.io/&quot;&gt;Honeybadger&lt;/a&gt; to track our errors, and while
they &lt;a href=&quot;https://docs.honeybadger.io/guides/integrations.html&quot;&gt;offer many integrations&lt;/a&gt;,
Basecamp isn’t one of them — enter &lt;a href=&quot;https://zapier.com&quot;&gt;Zapier&lt;/a&gt;.&lt;/p&gt;

&lt;h2 id=&quot;howd-i-do-it&quot;&gt;How’d I do it?&lt;/h2&gt;
&lt;p&gt;Zapier made it a very simple process:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;create a &lt;a href=&quot;https://zapier.com/apps/webhook/integrations&quot;&gt;catch webhook&lt;/a&gt; to catch &lt;a href=&quot;https://docs.honeybadger.io/guides/services.html#1-select-the-webhook-integration&quot;&gt;incoming webhooks from Honeybadger&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;create a POST webhook action that massages the JSON payload from Honeybadger, into &lt;a href=&quot;https://github.com/basecamp/bc3-api/blob/master/sections/chatbots.md#create-a-line&quot;&gt;the format Basecamp expects for a chatbot&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;

&lt;h3 id=&quot;the-result-in-basecamp&quot;&gt;The Result in Basecamp&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;/images/2019/07/honeybadger-chatbot1.png&quot; alt=&quot;Honeybadger Chatbot Example&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Using the &lt;code class=&quot;highlighter-rouge&quot;&gt;details&lt;/code&gt; and &lt;code class=&quot;highlighter-rouge&quot;&gt;summary&lt;/code&gt; elements gives you the ability in Basecamp to
create a collapsable post. In the sample above notice the little black right arrow,
if you click it you can even get a little sample of the stacktrace. Here’s the value
of the &lt;code class=&quot;highlighter-rouge&quot;&gt;contents&lt;/code&gt; key I used for the chatbot payload:&lt;/p&gt;

&lt;div class=&quot;overflow-x-scroll&quot;&gt;
  &lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-html&quot; data-lang=&quot;html&quot;&gt;    &lt;span class=&quot;nt&quot;&gt;&amp;lt;details&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;summary&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;💥({xxx__fault__id}){xxx__message}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;a&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;href=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;{xxx__fault__url}&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;View in Honeybadger&lt;span class=&quot;nt&quot;&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/summary&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;br&amp;gt;&amp;lt;hr&amp;gt;&amp;lt;br&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;pre&amp;gt;&lt;/span&gt;{xxx__notice__application_trace}&lt;span class=&quot;nt&quot;&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/details&amp;gt;&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;And here’s a preview of what the expanded preview looks like:&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/images/2019/07/honeybadger-chatbot2.png&quot; alt=&quot;Honeybadger Chatbot Example w/ Stacktrace&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;We are still in the process of fulling moving over to Basecamp, but so far I’ve not missed
Slack nearly as much as I thought I would.&lt;/p&gt;

&lt;p&gt;In all honestly the things we’ve missed
the most from Slack are the GIPHY integration, and the quick access to screen collaboration
tools, so if you have any suggestions on how your team addresses those areas, &lt;a href=&quot;https://twitter.com/kylekeesling&quot;&gt;I’d
love to hear about it&lt;/a&gt;.&lt;/p&gt;

                    
                </description>
            </item>
        
            <item>
                
                    <title>An Easier Way to Accommodate Deletion of ActiveStorage Attachments</title>
                    <link>https://kylekeesling.dev/posts/2018/12/activestorage-management-trick</link>
                    <guid isPermaLink="true">https://kylekeesling.dev/posts/2018/12/activestorage-management-trick</guid>
                

                <pubDate>Mon, 03 Dec 2018 17:45:00 -0500</pubDate>
                <description>
                    &lt;h2 id=&quot;a-little-background&quot;&gt;A Little Background&lt;/h2&gt;

&lt;p&gt;As I continue to phase out &lt;a href=&quot;https://github.com/thoughtbot/paperclip&quot;&gt;Paperclip&lt;/a&gt; in favor of &lt;a href=&quot;https://edgeguides.rubyonrails.org/active_storage_overview.html&quot;&gt;ActiveStorage&lt;/a&gt;, I’ve wanted to keep the methods I used to manage these assets as succinct
and reusable as possible.&lt;/p&gt;

&lt;p&gt;ActiveStorage gives us a dead simple way to save and update assets, but the ability to delete assets independently of the parent record, particularly if you’re using &lt;code class=&quot;highlighter-rouge&quot;&gt;has_many_attached&lt;/code&gt;, has been left to each individual app to figure out.&lt;/p&gt;

&lt;p&gt;I have stumbled upon a couple of different takes on how to potentially do this, but I’ve not been satisfied with their approaches. Many do not take into account authorization and permissions, which if you’re not careful, allows any user to delete any attachment they choose just by randomly hitting URLs in your application. With that in mind I set out to try and roll my own.&lt;/p&gt;

&lt;h2 id=&quot;the-approach&quot;&gt;The Approach&lt;/h2&gt;

&lt;p&gt;Since all attachments are saved as the same object/models types, &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveStorage/Attachment.html&quot;&gt;Attachment&lt;/a&gt; and &lt;a href=&quot;https://api.rubyonrails.org/classes/ActiveStorage/Blob.html&quot;&gt;Blob&lt;/a&gt;, we can use a common controller to interact and modify with them, regardless of the parent record that it belongs to.&lt;/p&gt;

&lt;p&gt;In my case I chose to stick closely to the naming conventions already given to us, so I created a new controller named &lt;code class=&quot;highlighter-rouge&quot;&gt;ActiveStorage::AttachmentsController&lt;/code&gt;, and since for now I’m only worried about deleting attachments, we only need one method, &lt;code class=&quot;highlighter-rouge&quot;&gt;delete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I use &lt;a href=&quot;https://github.com/plataformatec/devise&quot;&gt;Devise&lt;/a&gt; for authentication, so we need to make sure that the user is logged in before we let them do anything, which we do with &lt;code class=&quot;highlighter-rouge&quot;&gt;:authenticate_user!&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I also want to make sure that the user has the permissions to modify these attachments, so I check their permissions to see if they can modify the parent record using the &lt;code class=&quot;highlighter-rouge&quot;&gt;authorize_attachment_parent!&lt;/code&gt; method. I use &lt;a href=&quot;https://github.com/CanCanCommunity/cancancan&quot;&gt;CanCanCan&lt;/a&gt; in this case, but you can always swap out your auth call to fit whatever method you use. This piece is &lt;em&gt;&lt;strong&gt;critical&lt;/strong&gt;&lt;/em&gt; to ensure that your users aren’t deleting anything they shouldn’t.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;highlighter-rouge&quot;&gt;purge_later&lt;/code&gt; call will take care of the actual file deletion in your background queue, and will delete the corresponding &lt;code class=&quot;highlighter-rouge&quot;&gt;Blob&lt;/code&gt; record.&lt;/p&gt;

&lt;div class=&quot;overflow-x-scroll&quot;&gt;
  &lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# app/controllers/active_storage/attachments_controller.rb&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;nc&quot;&gt;ActiveStorage::AttachmentsController&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ApplicationController&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:authenticate_user!&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;before_action&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:set_attachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:authorize_attachment_parent!&lt;/span&gt;

      &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;destroy&lt;/span&gt;
        &lt;span class=&quot;vi&quot;&gt;@attachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;purge_later&lt;/span&gt;
      &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

      &lt;span class=&quot;kp&quot;&gt;private&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;set_attachment&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@attachment&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;ActiveStorage&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;::&lt;/span&gt;&lt;span class=&quot;no&quot;&gt;Attachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;params&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
          &lt;span class=&quot;vi&quot;&gt;@record&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@attachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;record&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;

        &lt;span class=&quot;k&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;authorize_attachment_parent!&lt;/span&gt;
          &lt;span class=&quot;n&quot;&gt;authorize!&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:manage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;vi&quot;&gt;@record&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;Also be sure to wire this new controller up in your routes:&lt;/p&gt;

&lt;div class=&quot;overflow-x-scroll&quot;&gt;
  &lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;# config/routes.rb&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;scope&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:active_storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;module: :active_storage&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;as: :active_storage&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do&lt;/span&gt;
      &lt;span class=&quot;n&quot;&gt;resources&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;:attachments&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;only: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;ss&quot;&gt;:destroy&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;end&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;It’s also important to update the user interface to remove any reference to the attachment, and in this case I used Rails UJS. The javascript necessary to remove the element from my UI is simple and straightforward:&lt;/p&gt;

&lt;div class=&quot;overflow-x-scroll&quot;&gt;
  &lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-javascript&quot; data-lang=&quot;javascript&quot;&gt;    &lt;span class=&quot;c1&quot;&gt;// app/views/active_storage/attachments/destroy.js.erb&lt;/span&gt;
    &lt;span class=&quot;nb&quot;&gt;document&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getElementById&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;lt;%= dom_id @attachment %&amp;gt;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;).&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;remove&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;/div&gt;

&lt;p&gt;The only assumptions made here are that you had the representation of your attachment wrapped in a div with ID of &lt;code class=&quot;highlighter-rouge&quot;&gt;attachment_#{id}&lt;/code&gt;, which you can easily render using Rails handy &lt;code class=&quot;highlighter-rouge&quot;&gt;dom_id&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;You can now use the following markup next all of your attachments to allow for easy deletion:&lt;/p&gt;

&lt;div class=&quot;overflow-x-scroll&quot;&gt;
  &lt;figure class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-ruby&quot; data-lang=&quot;ruby&quot;&gt;    &lt;span class=&quot;n&quot;&gt;link_to&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;Delete&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;active_storage_attachment_path&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;attachment&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
             &lt;span class=&quot;ss&quot;&gt;method: :delete&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;remote: :true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
             &lt;span class=&quot;ss&quot;&gt;data: &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;ss&quot;&gt;confirm: &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;Are you sure you wanna this?&quot;&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
  &lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
 &lt;/div&gt;

&lt;h2 id=&quot;wrapping-it-up&quot;&gt;Wrapping it Up&lt;/h2&gt;
&lt;p&gt;This approach is relatively simple, and relies heavily on the tools and features that Rails provides to us.&lt;/p&gt;

&lt;p&gt;In my case I’m also leveraging a common partial to render thumbnails, metadata, and links for attachments, making it dead simple to add attachments to any model I choose to in the future.&lt;/p&gt;

&lt;p&gt;As always things evolve over time, and while I’d like ot think I’m perfect I’m sure there are ways to improve this code, so if you have any suggestions or improvements &lt;a href=&quot;https://twitter.com/kylekeesling&quot;&gt;I’d love to hear them&lt;/a&gt;.&lt;/p&gt;

                    
                </description>
            </item>
        
    </channel>
</rss>