<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Untitled Publication]]></title><description><![CDATA[Untitled Publication]]></description><link>https://sergiotapia.com</link><generator>RSS for Node</generator><lastBuildDate>Mon, 13 Apr 2026 01:31:07 GMT</lastBuildDate><atom:link href="https://sergiotapia.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Sending your deploy markers to Appsignal from a Phoenix app.]]></title><description><![CDATA[AppSignal deploy markers are a way to keep track of what git commit the bug happened, or when the slowdown started appearing in your app.

https://docs.appsignal.com/application/markers/deploy-markers.html
As long as you have an APP_REVISION environm...]]></description><link>https://sergiotapia.com/sending-your-deploy-markers-to-appsignal-from-a-phoenix-app</link><guid isPermaLink="true">https://sergiotapia.com/sending-your-deploy-markers-to-appsignal-from-a-phoenix-app</guid><category><![CDATA[AppSignal]]></category><category><![CDATA[Elixir]]></category><category><![CDATA[Phoenix framework]]></category><category><![CDATA[railway]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Mon, 18 Nov 2024 13:35:52 GMT</pubDate><content:encoded><![CDATA[<p>AppSignal deploy markers are a way to keep track of what git commit the bug happened, or when the slowdown started appearing in your app.</p>
<p><img src="https://elixirforum.com/uploads/default/original/3X/9/1/918e41d27e398b3bd55b2144cd59573dc905608e.png" alt="image" /></p>
<p><a target="_blank" href="https://docs.appsignal.com/application/markers/deploy-markers.html">https://docs.appsignal.com/application/markers/deploy-markers.html</a></p>
<p>As long as you have an <code>APP_REVISION</code> environment variable set, you will send it to Appsignal through the integration library.</p>
<p>In the case of <a target="_blank" href="https://railway.com">Railway</a> you can quite easily add the variable to your Dockerfile and have everything work out nicely in 2 lines.</p>
<p>Railway even provides the variable automatically for you.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># At the very end of your Dockerfile:</span>

<span class="hljs-comment"># This is a variable Railway automatically exposes.</span>
<span class="hljs-comment"># Add the variable.</span>
<span class="hljs-keyword">ARG</span> RAILWAY_GIT_COMMIT_SHA 

<span class="hljs-comment"># Use the variable</span>
<span class="hljs-keyword">ENV</span> APP_REVISION=$RAILWAY_GIT_COMMIT_SHA

<span class="hljs-keyword">CMD</span><span class="bash"> [<span class="hljs-string">"/app/bin/server"</span>]</span>
</code></pre>
<p>And that's it. Deploy markers will now work for you.</p>
]]></content:encoded></item><item><title><![CDATA[Deploy Your Phoenix App on DigitalOcean in 30 Minutes]]></title><description><![CDATA[Hey there, we're going to walk through deploying a Phoenix app to a DigitalOcean droplet, manually - no tools no nothing. Just straight up BASH! But don't worry, I've done the hard work so you don't have to.
It occurred to me this week after seeing s...]]></description><link>https://sergiotapia.com/deploy-your-phoenix-app-on-digitalocean-in-30-minutes</link><guid isPermaLink="true">https://sergiotapia.com/deploy-your-phoenix-app-on-digitalocean-in-30-minutes</guid><category><![CDATA[Phoenix framework]]></category><category><![CDATA[DigitalOcean]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Thu, 29 Aug 2024 00:59:41 GMT</pubDate><content:encoded><![CDATA[<p>Hey there, we're going to walk through deploying a Phoenix app to a DigitalOcean droplet, manually - no tools no nothing. Just straight up BASH! But don't worry, I've done the hard work so you don't have to.</p>
<p>It occurred to me this week after seeing some devs on twitter arguing about vercel/self-hosted/serveless that I have ever deployed a Phoenix app myself, by hand, the good old fashioned way. I've always used a PaaS, be that Gigalixir, Heroku, Render, Railway, Fly. The last time I manually deployed something was with PHP in 2012.</p>
<p>Maybe the newer generation of devs know no other way, so I want you to take 30 minutes of your day today, give this a shot, and take a peek at how the sausage is made. It's not as scary as you think!</p>
<p><strong>This is not a production ready server. Missing: firewals, security, domains, ssl, rolling deploys, backups, etc.</strong></p>
<h2 id="heading-the-setup-creating-our-phoenix-project">The Setup: Creating Our Phoenix Project</h2>
<p>First things first, let's whip up a new Phoenix project. We're going with LiveView because, well, it's awesome, and we're using binary IDs because we're cool like that. Fire up your terminal and let's get cracking:</p>
<pre><code class="lang-bash">mix phx.new pokemon --binary-id --live
<span class="hljs-built_in">cd</span> pokemon
</code></pre>
<h2 id="heading-setting-up-our-digital-ocean-droplet">Setting Up Our Digital Ocean Droplet</h2>
<p>Alright, now we're going to set up our cozy little home in the cloud. Head over to DigitalOcean and create a new Droplet. Here's what you want:</p>
<ul>
<li><p>Region: New York</p>
</li>
<li><p>Datacenter: 1</p>
</li>
<li><p>OS: Ubuntu 24.04 LTS x264</p>
</li>
<li><p>Plan: Regular SSD, $4/month</p>
</li>
</ul>
<p>Set up root password authentication and make sure to copy that IPv4 address. You're gonna need it!</p>
<h2 id="heading-connecting-to-our-droplet">Connecting to Our Droplet</h2>
<p>Time to make friends with our new Droplet. In your terminal, type:</p>
<pre><code class="lang-bash">ssh root@your-ip
</code></pre>
<p>Accept that SSH key fingerprint (it's cool, trust me) and enter your root password. Boom! You're in.</p>
<h2 id="heading-updating-our-system">Updating Our System</h2>
<p>Let's make sure our new cloud home is up to date:</p>
<pre><code class="lang-bash">sudo apt update &amp;&amp; sudo apt upgrade -y
</code></pre>
<h2 id="heading-installing-postgresql-16">Installing PostgreSQL 16</h2>
<p>Now, we need a place to store all our Pokémon data. Enter PostgreSQL 16:</p>
<pre><code class="lang-bash">sudo sh -c <span class="hljs-string">'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" &gt; /etc/apt/sources.list.d/pgdg.list'</span>
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add -
sudo apt update
sudo apt install postgresql-16 -y
</code></pre>
<p>Start it up and make sure it runs on boot:</p>
<pre><code class="lang-bash">sudo systemctl start postgresql
sudo systemctl <span class="hljs-built_in">enable</span> postgresql
</code></pre>
<h2 id="heading-configuring-postgresql">Configuring PostgreSQL</h2>
<p>Time to set up our database. We're going with 'pokemon' as our password because, well, why not?</p>
<pre><code class="lang-bash">sudo -u postgres psql -c <span class="hljs-string">"ALTER USER postgres PASSWORD 'pokemon';"</span>
</code></pre>
<p>Now, let's make PostgreSQL accessible from anywhere (don't worry, we'll secure it later):</p>
<pre><code class="lang-bash">sudo nano /etc/postgresql/16/main/postgresql.conf
<span class="hljs-comment"># Change: listen_addresses = '*'</span>

sudo nano /etc/postgresql/16/main/pg_hba.conf
<span class="hljs-comment"># Add: host all all 0.0.0.0/0 md5</span>
</code></pre>
<p>Restart PostgreSQL and open up the firewall:</p>
<pre><code class="lang-bash">sudo systemctl restart postgresql
sudo ufw allow 5432/tcp
</code></pre>
<p>Create our app's database:</p>
<pre><code class="lang-bash">sudo -u postgres psql
CREATE DATABASE pokemon;
CREATE USER pokemon WITH PASSWORD <span class="hljs-string">'pokemon'</span>;
GRANT ALL PRIVILEGES ON DATABASE pokemon TO pokemon;
\q
</code></pre>
<h2 id="heading-installing-dependencies">Installing Dependencies</h2>
<p>Our Droplet needs some tools to run our Phoenix app:</p>
<pre><code class="lang-bash">sudo apt install -y build-essential inotify-tools
sudo apt install elixir
</code></pre>
<h2 id="heading-setting-up-caddy">Setting Up Caddy</h2>
<p>We're using Caddy as our web server because it's simple and awesome. Let's install it:</p>
<pre><code class="lang-bash">sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'</span> | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf <span class="hljs-string">'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'</span> | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
</code></pre>
<p>Start it up:</p>
<pre><code class="lang-bash">sudo systemctl start caddy
sudo systemctl <span class="hljs-built_in">enable</span> caddy
</code></pre>
<h2 id="heading-preparing-our-app-for-deployment">Preparing Our App for Deployment</h2>
<p>Back on your local machine, let's get our Phoenix app ready for the big leagues:</p>
<pre><code class="lang-bash">mix assets.deploy
MIX_ENV=prod mix compile
MIX_ENV=prod mix phx.digest
MIX_ENV=prod mix release
tar -czf pokemon.tar.gz -C _build/prod/rel/pokemon .
</code></pre>
<p>Now, let's send it to our Droplet:</p>
<pre><code class="lang-bash">scp pokemon.tar.gz root@your_droplet_ip:/opt/pokemon/
</code></pre>
<h2 id="heading-deploying-our-app">Deploying Our App</h2>
<p>On the Droplet, extract our app:</p>
<pre><code class="lang-bash">tar -xzf /opt/pokemon/pokemon.tar.gz -C /opt/pokemon
</code></pre>
<p>Configure Caddy to serve our app:</p>
<pre><code class="lang-bash">sudo nano /etc/caddy/Caddyfile
</code></pre>
<p>Add this to the Caddyfile:</p>
<pre><code class="lang-ruby"><span class="hljs-symbol">:</span><span class="hljs-number">80</span> {
    root * <span class="hljs-regexp">/opt/pokemon</span>
    file_server
    try_files {path} {path}/ <span class="hljs-regexp">/index.html
    reverse_proxy localhost:4000
}</span>
</code></pre>
<p>Restart Caddy:</p>
<pre><code class="lang-bash">sudo systemctl restart caddy
</code></pre>
<p>Finally, start our Phoenix app:</p>
<pre><code class="lang-bash">/opt/pokemon/bin/pokemon start
</code></pre>
<p>And there you have it, folks! Your Phoenix app should now be live on your Droplet's IP address. Go ahead, give it a visit in your browser. You should see your app running in all its glory!</p>
<p>So now you guys know how the sausage is made. You've peeked behind the curtain and perhaps gained new appreciation to what PaaS providers abstract away for you. Or maybe you decided this is totally easy and something you can handle yourself. Either way the choice is your now and you're informed.</p>
<p>Where to next?</p>
<p>The next step beyond this simple first implementation would be to use something like <a target="_blank" href="https://github.com/basecamp/kamal">Kamal</a> or <a target="_blank" href="https://github.com/coollabsio/coolify">Coolify</a>. Beyond that use a PaaS like <a target="_blank" href="http://Fly.io">Fly.io</a></p>
]]></content:encoded></item><item><title><![CDATA[Using binary ids / UUIDs for database primary keys in Rails.]]></title><description><![CDATA[It's generally a good idea to use binary ids / UUIds for the primary keys in your database.
Here's how to easily do it in Rails 7, using Postgres!
Configure your generators to use UUID for keys.
Create a file in config/initializers/generators.rb
Rail...]]></description><link>https://sergiotapia.com/using-binary-ids-uuids-for-database-primary-keys-in-rails</link><guid isPermaLink="true">https://sergiotapia.com/using-binary-ids-uuids-for-database-primary-keys-in-rails</guid><category><![CDATA[Rails]]></category><category><![CDATA[activerecord]]></category><category><![CDATA[uuid]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Wed, 05 Jun 2024 15:14:25 GMT</pubDate><content:encoded><![CDATA[<p>It's generally a good idea to use binary ids / UUIds for the primary keys in your database.</p>
<p>Here's how to easily do it in Rails 7, using Postgres!</p>
<h1 id="heading-configure-your-generators-to-use-uuid-for-keys">Configure your generators to use UUID for keys.</h1>
<p>Create a file in <code>config/initializers/generators.rb</code></p>
<pre><code class="lang-ruby">Rails.application.config.generators <span class="hljs-keyword">do</span> <span class="hljs-params">|g|</span>
  g.orm <span class="hljs-symbol">:active_record</span>, <span class="hljs-symbol">primary_key_type:</span> <span class="hljs-symbol">:uuid</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Now when you generate models it'll automatically use uuids for the primary keys. This even works automatically for things like action_text.</p>
<p>Here's what the action_text generator will create for you:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># This migration comes from action_text (originally 20180528164100)</span>
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateActionTextTables</span> &lt; ActiveRecord::Migration[6.0]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    <span class="hljs-comment"># Use Active Record's configured type for primary and foreign keys</span>
    primary_key_type, foreign_key_type = primary_and_foreign_key_types

    create_table <span class="hljs-symbol">:action_text_rich_texts</span>, <span class="hljs-symbol">id:</span> primary_key_type <span class="hljs-keyword">do</span> <span class="hljs-params">|t|</span>
      t.string     <span class="hljs-symbol">:name</span>, <span class="hljs-symbol">null:</span> <span class="hljs-literal">false</span>
      t.text       <span class="hljs-symbol">:body</span>, <span class="hljs-symbol">size:</span> <span class="hljs-symbol">:long</span>
      t.references <span class="hljs-symbol">:record</span>, <span class="hljs-symbol">null:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">polymorphic:</span> <span class="hljs-literal">true</span>, <span class="hljs-symbol">index:</span> <span class="hljs-literal">false</span>, <span class="hljs-symbol">type:</span> foreign_key_type

      t.timestamps

      t.index [ <span class="hljs-symbol">:record_type</span>, <span class="hljs-symbol">:record_id</span>, <span class="hljs-symbol">:name</span> ], <span class="hljs-symbol">name:</span> <span class="hljs-string">"index_action_text_rich_texts_uniqueness"</span>, <span class="hljs-symbol">unique:</span> <span class="hljs-literal">true</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>

  private
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">primary_and_foreign_key_types</span></span>
      config = Rails.configuration.generators
      setting = config.options[config.orm][<span class="hljs-symbol">:primary_key_type</span>]
      primary_key_type = setting <span class="hljs-params">||</span> <span class="hljs-symbol">:primary_key</span>
      foreign_key_type = setting <span class="hljs-params">||</span> <span class="hljs-symbol">:bigint</span>
      [primary_key_type, foreign_key_type]
    <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Notice the id type! Pretty nifty.</p>
<h1 id="heading-make-sure-your-foreign-keys-use-uuid">Make sure your foreign keys use UUID!</h1>
<p>When you start creating more tables, make sure the type is correct.</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CreateQuotes</span> &lt; ActiveRecord::Migration[7.1]</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">change</span></span>
    create_table <span class="hljs-symbol">:quotes</span>, <span class="hljs-symbol">id:</span> <span class="hljs-symbol">:uuid</span> <span class="hljs-keyword">do</span> <span class="hljs-params">|t|</span>
      t.text <span class="hljs-symbol">:favorite_quote</span>
      t.uuid <span class="hljs-symbol">:user_id</span>

      t.timestamps
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p><code>t.uuid</code>! Don't forget!</p>
<p>And it's that easy.</p>
]]></content:encoded></item><item><title><![CDATA[Direct File uploads with Phoenix Liveview and Cloudflare R2.]]></title><description><![CDATA[It's been really hard to figure this out and I had to cobble together many sources to get it working. Enjoy!

We're going to build a direct image upload to Cloudflare B2 from the browser. That way the file doesn't travel to your servers and slow them...]]></description><link>https://sergiotapia.com/direct-file-uploads-with-phoenix-liveview-and-cloudflare-r2</link><guid isPermaLink="true">https://sergiotapia.com/direct-file-uploads-with-phoenix-liveview-and-cloudflare-r2</guid><category><![CDATA[phoenix liveview]]></category><category><![CDATA[Cloudflare-r2]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Thu, 11 Jan 2024 15:45:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1704987868700/a3ed6717-1263-441d-8634-c99d00c3d92b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It's been really hard to figure this out and I had to cobble together many sources to get it working. Enjoy!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704986791701/eed952d1-1b8e-414b-b277-febe1e1b9cf0.png" alt class="image--center mx-auto" /></p>
<p>We're going to build a direct image upload to Cloudflare B2 from the browser. That way the file doesn't travel to your servers and slow them down.</p>
<h2 id="heading-create-your-bucket-and-set-cors">Create your bucket and set CORS</h2>
<p>You'll need to set your bucket to allow public access, and set some CORS rules to allow remote PUT requests.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704986934884/d13b2e5c-4bc0-4023-98a1-5fe2b94885d9.png" alt class="image--center mx-auto" /></p>
<p>And edit your CORS policy for that bucket:</p>
<pre><code class="lang-json">[
  {
    <span class="hljs-attr">"AllowedOrigins"</span>: [
      <span class="hljs-string">"http://localhost:4000"</span>,
      <span class="hljs-string">"https://app.onrender.com"</span>
    ],
    <span class="hljs-attr">"AllowedMethods"</span>: [
      <span class="hljs-string">"GET"</span>,
      <span class="hljs-string">"PUT"</span>,
      <span class="hljs-string">"POST"</span>
    ],
    <span class="hljs-attr">"AllowedHeaders"</span>: [
      <span class="hljs-string">"*"</span>
    ],
    <span class="hljs-attr">"ExposeHeaders"</span>: []
  }
]
</code></pre>
<p>Finally, you'll need these environment variables so set them for your project locally.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">export</span> CLOUDFRONT_ACCOUNT_ID=<span class="hljs-string">"xxx"</span>
<span class="hljs-built_in">export</span> CLOUDFRONT_BUCKET_NAME=<span class="hljs-string">"xxx"</span>
<span class="hljs-built_in">export</span> CLOUDFRONT_R2_ACCESS_KEY_ID=<span class="hljs-string">"xxx"</span>
<span class="hljs-built_in">export</span> CLOUDFRONT_R2_SECRET_ACCESS_KEY=<span class="hljs-string">"xxx"</span>
</code></pre>
<h2 id="heading-install-mix-deps">Install mix deps</h2>
<pre><code class="lang-elixir">{<span class="hljs-symbol">:ex_aws</span>, <span class="hljs-string">"~&gt; 2.5"</span>},
{<span class="hljs-symbol">:ex_aws_s3</span>, <span class="hljs-string">"~&gt; 2.5"</span>},
{<span class="hljs-symbol">:aws_signature</span>, <span class="hljs-string">"~&gt; 0.3.1"</span>}
</code></pre>
<h2 id="heading-create-simples3uploadex-helper-module">Create simple_s3_upload.ex helper module</h2>
<p>This file was modified by someone else, many thanks to him!</p>
<pre><code class="lang-elixir"><span class="hljs-class"><span class="hljs-keyword">defmodule</span> <span class="hljs-title">MyApp.SimpleS3Upload</span></span> <span class="hljs-keyword">do</span>
  <span class="hljs-variable">@moduledoc</span> <span class="hljs-string">"""
  Below is code from Chris McCord, modified for Cloudflare R2

  https://gist.github.com/chrismccord/37862f1f8b1f5148644b75d20d1cb073

  """</span>
  <span class="hljs-variable">@one_hour_seconds</span> <span class="hljs-number">3600</span>

  <span class="hljs-variable">@doc</span> <span class="hljs-string">"""
    Returns `{:ok, presigned_url}` where `presigned_url` is a url string

  """</span>
  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">presigned_put</span></span>(config, opts) <span class="hljs-keyword">do</span>
    key = Keyword.fetch!(opts, <span class="hljs-symbol">:key</span>)
    expires_in = Keyword.get(opts, <span class="hljs-symbol">:expires_in</span>, <span class="hljs-variable">@one_hour_seconds</span>)
    uri = <span class="hljs-string">"<span class="hljs-subst">#{config.url}</span>/<span class="hljs-subst">#{URI.encode(key)}</span>"</span>

    url =
      <span class="hljs-symbol">:aws_signature</span>.sign_v4_query_params(
        config.access_key_id,
        config.secret_access_key,
        config.region,
        <span class="hljs-string">"s3"</span>,
        <span class="hljs-symbol">:calendar</span>.universal_time(),
        <span class="hljs-string">"PUT"</span>,
        uri,
        <span class="hljs-symbol">ttl:</span> expires_in,
        <span class="hljs-symbol">uri_encode_path:</span> <span class="hljs-keyword">false</span>,
        <span class="hljs-symbol">body_digest:</span> <span class="hljs-string">"UNSIGNED-PAYLOAD"</span>
      )

    {<span class="hljs-symbol">:ok</span>, url}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<hr />
<p>Chin up boys, that was the hard part. Let's push some files!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1704987241885/f6edc1bb-e1dd-4512-bab2-223252ffaa2d.gif" alt class="image--center mx-auto" /></p>
<hr />
<h2 id="heading-allowupload-for-your-socket">Allow_upload for your socket</h2>
<p>We need to let liveview know we want some files.</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">update</span></span>(assigns, socket) <span class="hljs-keyword">do</span>
  {<span class="hljs-symbol">:ok</span>,
   socket
   |&gt; assign(<span class="hljs-symbol">:uploaded_files</span>, [])
   |&gt; allow_upload(<span class="hljs-symbol">:profile_picture</span>,
     <span class="hljs-symbol">accept:</span> <span class="hljs-string">~w(.jpg .jpeg .png)</span>,
     <span class="hljs-symbol">max_entries:</span> <span class="hljs-number">2</span>,
     <span class="hljs-symbol">external:</span> &amp;presign_upload/<span class="hljs-number">2</span>
   )
   |&gt; assign_form(changeset)}
<span class="hljs-keyword">end</span>

<span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">presign_upload</span></span>(entry, socket) <span class="hljs-keyword">do</span>
  filename = <span class="hljs-string">"<span class="hljs-subst">#{entry.client_name}</span>"</span>
  key = <span class="hljs-string">"public/<span class="hljs-subst">#{Nanoid.generate()}</span>-<span class="hljs-subst">#{filename}</span>"</span>

  config = %{
    <span class="hljs-symbol">region:</span> <span class="hljs-string">"auto"</span>,
    <span class="hljs-symbol">access_key_id:</span> System.get_env(<span class="hljs-string">"CLOUDFRONT_R2_ACCESS_KEY_ID"</span>),
    <span class="hljs-symbol">secret_access_key:</span> System.get_env(<span class="hljs-string">"CLOUDFRONT_R2_SECRET_ACCESS_KEY"</span>),
    <span class="hljs-symbol">url:</span>
      <span class="hljs-string">"https://<span class="hljs-subst">#{System.get_env(<span class="hljs-string">"CLOUDFRONT_BUCKET_NAME"</span>)}</span>.<span class="hljs-subst">#{System.get_env(<span class="hljs-string">"CLOUDFRONT_ACCOUNT_ID"</span>)}</span>.r2.cloudflarestorage.com"</span>
  }

  {<span class="hljs-symbol">:ok</span>, presigned_url} =
    MyApp.SimpleS3Upload.presigned_put(config,
      <span class="hljs-symbol">key:</span> key,
      <span class="hljs-symbol">content_type:</span> entry.client_type,
      <span class="hljs-symbol">max_file_size:</span> socket.assigns.uploads[entry.upload_config].max_file_size
    )

  meta = %{
    <span class="hljs-symbol">uploader:</span> <span class="hljs-string">"S3"</span>,
    <span class="hljs-symbol">key:</span> key,
    <span class="hljs-symbol">url:</span> presigned_url
  }

  {<span class="hljs-symbol">:ok</span>, meta, socket}
<span class="hljs-keyword">end</span>
</code></pre>
<h2 id="heading-the-html-for-your-form">The HTML for your form.</h2>
<p>The dudes at Phoenix Framework core team built some awesome helpers for us. Don't reinvent the wheel!</p>
<pre><code class="lang-elixir"><span class="hljs-comment"># Inside your &lt;.simple_form...</span>
&lt;.live_file_input upload={<span class="hljs-variable">@uploads</span>.profile_picture} /&gt;
&lt;%= <span class="hljs-keyword">for</span> entry &lt;- <span class="hljs-variable">@uploads</span>.profile_picture.entries <span class="hljs-keyword">do</span> %&gt;
  &lt;article class=<span class="hljs-string">"upload-entry"</span>&gt;
    &lt;figure&gt;
      &lt;.live_img_preview entry={entry} /&gt;
      &lt;figcaption&gt;&lt;%= entry.client_name %&gt;&lt;<span class="hljs-regexp">/figcaption&gt;
    &lt;/figure</span>&gt;

    &lt;%!-- entry.progress will update automatically <span class="hljs-keyword">for</span> <span class="hljs-keyword">in</span>-flight entries --%&gt;
    &lt;progress value={entry.progress} max=<span class="hljs-string">"100"</span>&gt;&lt;%= entry.progress %&gt;%&lt;<span class="hljs-regexp">/progress&gt;

    &lt;%!-- a regular click event whose handler will invoke Phoenix.LiveView.cancel_upload/</span><span class="hljs-number">3</span> --%&gt;
    &lt;button
      type=<span class="hljs-string">"button"</span>
      phx-click=<span class="hljs-string">"cancel-upload"</span>
      phx-value-ref={entry.ref}
      phx-target={<span class="hljs-variable">@myself</span>}
      aria-label=<span class="hljs-string">"cancel"</span>
    &gt;
      &amp;times;
    &lt;<span class="hljs-regexp">/button&gt;

    &lt;%!-- Phoenix.Component.upload_errors/</span><span class="hljs-number">2</span> returns a list of error atoms --%&gt;
    &lt;%= <span class="hljs-keyword">for</span> err &lt;- upload_errors(<span class="hljs-variable">@uploads</span>.profile_picture, entry) <span class="hljs-keyword">do</span> %&gt;
      &lt;p class=<span class="hljs-string">"alert alert-danger"</span>&gt;&lt;%= inspect(err) %&gt;&lt;<span class="hljs-regexp">/p&gt;
    &lt;% end %&gt;
  &lt;/article</span>&gt;
&lt;% <span class="hljs-keyword">end</span> %&gt;
</code></pre>
<h2 id="heading-finally-saving-the-files-url-in-your-database-schema">Finally saving the file's URL in your database schema.</h2>
<p>Just <code>consume_uploaded_entries</code> and you will have access to the final URL.</p>
<pre><code class="lang-elixir"><span class="hljs-function"><span class="hljs-keyword">defp</span> <span class="hljs-title">save_company</span></span>(socket, <span class="hljs-symbol">:new</span>, company_params) <span class="hljs-keyword">do</span>
  uploaded_files =
    consume_uploaded_entries(socket, <span class="hljs-symbol">:profile_picture</span>, <span class="hljs-keyword">fn</span> %{<span class="hljs-symbol">key:</span> key}, _entry -&gt;
      <span class="hljs-string">"https://pub-757575757575757575.r2.dev/<span class="hljs-subst">#{key}</span>"</span>
    <span class="hljs-keyword">end</span>)

  company_params = Map.put(company_params, <span class="hljs-string">"profile_picture_url"</span>, List.first(uploaded_files))
  Companies.create_company(company_params) 
<span class="hljs-keyword">end</span>
</code></pre>
<p>And that it's. Enjoy significantly cheaper storage and no egress fees with Cloudflare R2 storage.</p>
<hr />
<h1 id="heading-if-this-helped-you-follow-me-on-twitterx">If this helped you, follow me on Twitter/X!</h1>
<p><a target="_blank" href="https://twitter.com/yeyoparadox">https://twitter.com/yeyoparadox</a></p>
]]></content:encoded></item><item><title><![CDATA[DropzoneJS direct uploads to Backblaze B2 in Phoenix Liveview.]]></title><description><![CDATA[Woah read that title again, what a mouthful.
I've spent the last five evenings after work trying to get this to work. I've cobbled together blog posts, forums posts, github issues, hell I've spelunked chinese stackoverflow content cloner websites try...]]></description><link>https://sergiotapia.com/dropzonejs-direct-uploads-to-backblaze-b2-in-phoenix-liveview</link><guid isPermaLink="true">https://sergiotapia.com/dropzonejs-direct-uploads-to-backblaze-b2-in-phoenix-liveview</guid><category><![CDATA[Phoenix framework]]></category><category><![CDATA[liveview]]></category><category><![CDATA[File Upload]]></category><category><![CDATA[Dropzone.js]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Wed, 31 Aug 2022 15:33:31 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1661959883020/Q03vP96ot.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Woah read that title again, what a mouthful.</p>
<p>I've spent the last five evenings after work trying to get this to work. I've cobbled together blog posts, forums posts, github issues, hell I've spelunked chinese stackoverflow content cloner websites trying to figure this out. </p>
<p><strong>It was a pain in the ass!</strong></p>
<p>I'm going to teach you how to presign a Backblaze B2 upload url using the <code>b2_get_upload_url</code> API method. We're then going to use that upload url to directly upload a file to your Backblaze B2 bucket from the browser.</p>
<p>Sounds simple right? Christ...</p>
<p>I hope this guide saves a collective thousands of hours out there. If this article helped you leave a comment please, I always enjoy reading those.</p>
<hr />
<h1 id="heading-create-your-backblaze-b2-bucket-and-set-up-cors">Create your Backblaze B2 bucket and set up CORS.</h1>
<p>I'm not going to teach you how to create a bucket. Once you have your bucket, you need to use the Backblaze CLI to set up CORS for it. <strong>You cannot set up the right CORS rules in the Backblaze UI</strong>.</p>
<p>I'm using Linux, so I downloaded the Backblaze CLI and here's how I run it.</p>
<p>Make sure you have the right environment variables set before running the <code>b2-linux</code> binary.</p>
<pre><code><span class="hljs-built_in">export</span> B2_APPLICATION_KEY_ID=<span class="hljs-string">""</span>
<span class="hljs-built_in">export</span> B2_APPLICATION_KEY=<span class="hljs-string">""</span>
<span class="hljs-built_in">export</span> B2_APPLICATION_KEY_NAME=<span class="hljs-string">"my-awesome-bucket-name"</span>
</code></pre><p>Then update the CORS rules. Note that you <strong>cannot</strong> pass in a filename like <code>foobar.json</code> you <strong>must pass in the content itself</strong>. Lots of people trip with this one.</p>
<pre><code><span class="hljs-comment"># I'm assuming you have the `b2-linux` binary in your folder...</span>
./b2-linux <span class="hljs-keyword">update</span>-<span class="hljs-keyword">bucket</span> <span class="hljs-comment">--corsRules '[</span>
    {
        <span class="hljs-string">"corsRuleName"</span>: <span class="hljs-string">"downloadFromAnyOriginWithUpload"</span>,
        <span class="hljs-string">"allowedOrigins"</span>: [
            <span class="hljs-string">"*"</span>
        ],
        <span class="hljs-string">"allowedHeaders"</span>: [
            <span class="hljs-string">"*"</span>
        ],
        <span class="hljs-string">"allowedOperations"</span>: [
            <span class="hljs-string">"b2_download_file_by_id"</span>,
            <span class="hljs-string">"b2_download_file_by_name"</span>,
            <span class="hljs-string">"b2_upload_file"</span>,
            <span class="hljs-string">"b2_upload_part"</span>
        ],
        <span class="hljs-string">"maxAgeSeconds"</span>: <span class="hljs-number">3600</span>
    }
]<span class="hljs-string">' my-awesome-bucket-name allPublic</span>
</code></pre><p>Now your bucket is ready to receive XHR from the browser.</p>
<h1 id="heading-presigning-urls">Presigning URLs.</h1>
<p>We're going to create a <code>backblaze.ex</code> module to do the presigning. I'm using <a target="_blank" href="https://hex.pm/packages/req">req</a> you can use whatever HTTP library you want.</p>
<pre><code>defmodule MyApp.Backblaze do
  @b2_application_key System.get_env(<span class="hljs-string">"B2_APPLICATION_KEY"</span>)
  @b2_application_key_id System.get_env(<span class="hljs-string">"B2_APPLICATION_KEY_ID"</span>)
  @b2_application_key_name System.get_env(<span class="hljs-string">"B2_APPLICATION_KEY_NAME"</span>)
  @b2_bucket_id System.get_env(<span class="hljs-string">"B2_BUCKET_ID"</span>)

  def get_upload_url() do
    <span class="hljs-operator">%</span>{api_url: api_url, authorization_token: authorization_token} <span class="hljs-operator">=</span>
      get_api_url_and_authorization_token()

    request <span class="hljs-operator">=</span>
      Req.post!(<span class="hljs-string">"#{api_url}/b2api/v2/b2_get_upload_url"</span>,
        headers: [{<span class="hljs-string">"authorization"</span>, <span class="hljs-string">"#{authorization_token}"</span>}],
        json: <span class="hljs-operator">%</span>{bucketId: @b2_bucket_id}
      )

    request.body
  end

  def get_api_url_and_authorization_token() do
    auth_base_64 <span class="hljs-operator">=</span>
      <span class="hljs-string">"#{@b2_application_key_id}:#{@b2_application_key}"</span>
      <span class="hljs-operator">|</span><span class="hljs-operator">&gt;</span> Base.encode64()

    response <span class="hljs-operator">=</span>
      Req.get!(<span class="hljs-string">"https://api.backblazeb2.com/b2api/v2/b2_authorize_account"</span>,
        headers: [{<span class="hljs-string">"authorization"</span>, <span class="hljs-string">"Basic #{auth_base_64}"</span>}]
      )

    api_url <span class="hljs-operator">=</span> response.body[<span class="hljs-string">"apiUrl"</span>]
    authorization_token <span class="hljs-operator">=</span> response.body[<span class="hljs-string">"authorizationToken"</span>]

    <span class="hljs-operator">%</span>{api_url: api_url, authorization_token: authorization_token}
  end
end
</code></pre><p>We also need an endpoint we can hit from the frontend.</p>
<pre><code>scope <span class="hljs-string">"/api"</span>, MyAppWeb <span class="hljs-keyword">do</span>
  pipe_through <span class="hljs-symbol">:api</span>

  post <span class="hljs-string">"/presign-upload-url"</span>, PresignController, <span class="hljs-symbol">:presign</span>
<span class="hljs-keyword">end</span>
</code></pre><p>And the controller.</p>
<pre><code>defmodule MyAppWeb.PresignController <span class="hljs-keyword">do</span>
  use MyAppWeb, <span class="hljs-symbol">:controller</span>
  <span class="hljs-keyword">alias</span> MyApp.Backblaze

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">presign</span><span class="hljs-params">(conn, _params)</span></span> <span class="hljs-keyword">do</span>
    upload_url = Backblaze.get_upload_url()
    json(conn, <span class="hljs-string">%{upload_url: upload_url}</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre><h1 id="heading-install-dropzonejs">Install DropzoneJS</h1>
<p>Add <code>dropzone</code> to your <code>package.json</code> and <code>npm i</code> from inside of your <code>/assets</code> folder.</p>
<pre><code>{
  <span class="hljs-attr">"devDependencies"</span>: {
    <span class="hljs-attr">"@tailwindcss/forms"</span>: <span class="hljs-string">"^0.5.2"</span>
  },
  <span class="hljs-attr">"dependencies"</span>: {
    <span class="hljs-attr">"alpinejs"</span>: <span class="hljs-string">"^3.10.3"</span>,
    <span class="hljs-attr">"dropzone"</span>: <span class="hljs-string">"^6.0.0-beta.2"</span>
  }
}
</code></pre><h1 id="heading-install-the-dropzonejs-css">Install the DropzoneJS css</h1>
<p>Go to <code>app.css</code> and the import for Dropzone's styles.</p>
<pre><code><span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/base"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/components"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/utilities"</span>;

 <span class="hljs-comment">/* ADD THIS */</span>
<span class="hljs-keyword">@import</span> <span class="hljs-string">"dropzone/dist/dropzone.css"</span>;
</code></pre><h1 id="heading-add-dropzonejs-to-your-hooks">Add DropzoneJS to your hooks.</h1>
<p>In <code>app.js</code> you want to import a file we are going to create in the next step.</p>
<pre><code>// We're going to <span class="hljs-keyword">create</span> this <span class="hljs-keyword">file</span> <span class="hljs-keyword">in</span> the <span class="hljs-keyword">next</span> step!
<span class="hljs-keyword">import</span> dropzone <span class="hljs-keyword">from</span> <span class="hljs-string">"./dropzone"</span>;
</code></pre><p>And add it to your <code>hooks</code> object.</p>
<pre><code>let hooks <span class="hljs-operator">=</span> {};

<span class="hljs-comment">// Add this...</span>
hooks.Dropzone <span class="hljs-operator">=</span> dropzone;

let liveSocket <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> LiveSocket(<span class="hljs-string">"/live"</span>, Socket, {
  params: { _csrf_token: csrfToken },
  hooks: hooks,
  dom: {
    onBeforeElUpdated(<span class="hljs-keyword">from</span>, to) {
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">from</span>._x_dataStack) {
        window.Alpine.clone(<span class="hljs-keyword">from</span>, to);
      }
    },
  },
});
</code></pre><h1 id="heading-prepare-your-dropzonejs-form">Prepare your DropzoneJS form</h1>
<p>In whatever heex template you have, add a form.</p>
<pre><code><span class="hljs-operator">&lt;</span>form
  class<span class="hljs-operator">=</span><span class="hljs-string">"dropzone dz-clickable"</span>
  id<span class="hljs-operator">=</span><span class="hljs-string">"dropzone"</span>
  phx<span class="hljs-operator">-</span>hook<span class="hljs-operator">=</span><span class="hljs-string">"Dropzone"</span>
  phx<span class="hljs-operator">-</span>update<span class="hljs-operator">=</span><span class="hljs-string">"ignore"</span>
  enctype<span class="hljs-operator">=</span><span class="hljs-string">"multipart/form-data"</span>
<span class="hljs-operator">&gt;</span>
<span class="hljs-operator">&lt;</span><span class="hljs-operator">/</span>form<span class="hljs-operator">&gt;</span>
</code></pre><p>Alright we're done with all of the ceremony, now it's time to actually <strong>upload</strong>. Are you ready? We're almost there. Celebrate!</p>
<p><img src="https://c.tenor.com/_cY2OJVxVeoAAAAC/shoot-angry.gif" alt="rambo.gif" /></p>
<h1 id="heading-create-your-dropzonejs-file">Create your dropzone.js file</h1>
<p>This should live in <code>assets/js/dropzone.js</code>. </p>
<pre><code><span class="hljs-keyword">import</span> <span class="hljs-title">Dropzone</span> <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"dropzone"</span>;

export default {
  mounted() {
    let csrfToken <span class="hljs-operator">=</span> document
      .querySelector(<span class="hljs-string">"meta[name='csrf-token']"</span>)
      .getAttribute(<span class="hljs-string">"content"</span>);

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initUpload</span>(<span class="hljs-params">file</span>) </span>{
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Promise(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">resolve, reject</span>) </span>{
        fetch(<span class="hljs-string">"/api/presign-upload-url"</span>, {
          headers: {
            <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
            <span class="hljs-string">"Accept"</span>: <span class="hljs-string">"application/json"</span>,
            <span class="hljs-string">"X-Requested-With"</span>: <span class="hljs-string">"XMLHttpRequest"</span>,
            <span class="hljs-string">"X-CSRF-Token"</span>: csrfToken
          },
          method: <span class="hljs-string">"post"</span>,
          credentials: <span class="hljs-string">"same-origin"</span>,
          body: JSON.stringify({
            key: file.<span class="hljs-built_in">name</span>
          })
        })
          .then(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">response</span>) </span>{
            resolve(response.json());
          });
      });
    }


    let myDropzone <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Dropzone(<span class="hljs-built_in">this</span>.el, {
      url: <span class="hljs-string">"#"</span>,
      method: <span class="hljs-string">"post"</span>,
      acceptedFiles: <span class="hljs-string">"image/*"</span>,
      autoProcessQueue: <span class="hljs-literal">true</span>,
      parallelUploads: <span class="hljs-number">1</span>,
      maxFilesize: <span class="hljs-number">25</span>, <span class="hljs-comment">// Megabytes</span>
      maxFiles: <span class="hljs-number">5</span>,
      uploadMultiple: <span class="hljs-literal">true</span>,
      transformFile: async <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file, done</span>) </span>{
        let initData <span class="hljs-operator">=</span> await initUpload(file);
        file.uploadUrl <span class="hljs-operator">=</span> initData.upload_url.uploadUrl;
        file.authorizationToken <span class="hljs-operator">=</span> initData.upload_url.authorizationToken;

        done(file);
      },
      init: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">this</span>.on(<span class="hljs-string">"sending"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file, xhr, formData</span>) </span>{
          xhr.open(myDropzone.options.method, file.uploadUrl);

          xhr.setRequestHeader(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"b2/x-auto"</span>);
          xhr.setRequestHeader(<span class="hljs-string">"Authorization"</span>, file.authorizationToken);
          <span class="hljs-comment">// If you want to upload to a "folder" you just set it as part of the X-Bz-File-Name</span>
          <span class="hljs-comment">// for example you can set the username from a `window.username` variable you set.</span>
          <span class="hljs-comment">// xhr.setRequestHeader("X-Bz-File-Name", `yeyo/${Date.now()}-${encodeURI(file.name)}`);</span>
          xhr.setRequestHeader(<span class="hljs-string">"X-Bz-File-Name"</span>, `${Date.now()}<span class="hljs-operator">-</span>${encodeURI(file.<span class="hljs-built_in">name</span>)}`);
          xhr.setRequestHeader(<span class="hljs-string">"X-Bz-Content-Sha1"</span>, <span class="hljs-string">"do_not_verify"</span>);

          let _send <span class="hljs-operator">=</span> xhr.<span class="hljs-built_in">send</span>
          xhr.<span class="hljs-built_in">send</span> <span class="hljs-operator">=</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
            _send.<span class="hljs-built_in">call</span>(xhr, file);
          }
        });

        <span class="hljs-built_in">this</span>.on(<span class="hljs-string">"success"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">file, response</span>) </span>{
          <span class="hljs-comment">// The response has all the info you need to build the URL for your uploaded</span>
          <span class="hljs-comment">// file. Use it to set values on a hidden field for your `changeset` for example </span>
          console.log(response.fileId);
        });
      }
    });
  },
};
</code></pre><p>Guys... Dropzone is so vast and literally thousands of hours have gone into it by many different people but the documentation is very lacking. Herculean effort by the team and I owe them a lot of gratitude. It was painful for me to get this working. It's very hard to discern what to do and many of the options contradict other options and make it behave strange.</p>
<p>Even the example this article has some weirdness: you can't set <code>parallelUploads: true</code> otherwise only the last image actually gets uploaded. Weird right? But I'm happy with where this is today.</p>
<p>Hope this saved you at least an hour. If this didn't work for you, please leave a comment and I'll try to help out. </p>
<p>As next steps, throw up an <a target="_blank" href="https://imgproxy.net/">imgproxy</a> in front of your bucket for on the fly transformations. Then... throw up Bunny CDN in front of <em>that</em> for speed and lower your costs even more. </p>
<p><img src="https://c.tenor.com/umGBhNv5L8YAAAAC/tie-knot.gif" alt="rambo.gif" /></p>
<p>Now get to uploading and save big bucks by using Backblaze B2.</p>
]]></content:encoded></item><item><title><![CDATA[Best FFmpeg Cookbook: My Go-To Recipes]]></title><description><![CDATA[I love FFmpeg. https://ffmpeg.org/
I think it's one of the wonders of the world. A feat in engineering that has had so many people working on it, too powerful for one person to know every nook and cranny. It's gargantuan and incredibly complex.
It al...]]></description><link>https://sergiotapia.com/my-favorite-ffmpeg-cookbook</link><guid isPermaLink="true">https://sergiotapia.com/my-favorite-ffmpeg-cookbook</guid><category><![CDATA[General Programming]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Fri, 11 Mar 2022 01:14:10 GMT</pubDate><content:encoded><![CDATA[<p>I love FFmpeg. https://ffmpeg.org/</p>
<p>I think it's one of the wonders of the world. A feat in engineering that has had so many people working on it, too powerful for one person to know every nook and cranny. It's gargantuan and incredibly complex.</p>
<p>It also fits too many use cases to count!</p>
<p>Here's my cookbook for FFmpeg. Useful recipes I use almost daily to share things online with friends, archive for myself and otherwise fiddle around with things.</p>
<h1 id="heading-create-a-timelapse-using-pictures-in-a-folder">Create a timelapse using pictures in a folder.</h1>
<pre><code class="lang-powershell">ffmpeg <span class="hljs-literal">-framerate</span> <span class="hljs-number">30</span> <span class="hljs-literal">-pattern_type</span> glob <span class="hljs-literal">-i</span> <span class="hljs-string">'*.JPG'</span> <span class="hljs-literal">-c</span>:v libx264 <span class="hljs-literal">-r</span> <span class="hljs-number">30</span> <span class="hljs-literal">-pix_fmt</span> yuv420p timelapse.mp4
</code></pre>
<h1 id="heading-add-meme-caption-to-videos">Add meme caption to videos.</h1>
<pre><code class="lang-plaintext">ffmpeg -i "video.mp4" -vf "drawtext=text='TOP TEXT':fontfile='C\:\\Windows\\Fonts\\arial.ttf':fontcolor=white:fontsize=72:borderw=5:x=(w-text_w)/2:y=(h*0.05)" -c:a copy output_with_top_text.mp4

ffmpeg -i "video.mp4" -vf "drawtext=text='ROFL':fontfile='C\:\\Windows\\Fonts\\arial.ttf':fontcolor=white:fontsize=72:borderw=5:x=(w-text_w)/2:y=(h*0.05), drawtext=text='LMAO':fontfile='C\:\\Windows\\Fonts\\arial.ttf':fontcolor=white:fontsize=72:borderw=5:x=(w-text_w)/2:y=(h-text_h)-(h*0.05)" -c:a copy output_with_text.mp4
</code></pre>
<h1 id="heading-convert-video-to-twitter-friendly-format">Convert video to Twitter friendly format.</h1>
<pre><code class="lang-plaintext">ffmpeg -i video.mp4 -vcodec libx264 -pix_fmt yuv420p -strict experimental -r 30 -t 2:20 -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" -vb 1024k -acodec aac -ar 44100 -ac 2 -minrate 1024k -maxrate 1024k -bufsize 1024k -movflags +faststart output.mp4
</code></pre>
<h1 id="heading-convert-video-to-whatsapp-friendly-format">Convert video to WhatsApp friendly format.</h1>
<pre><code class="lang-plaintext">ffmpeg.exe -i video.mp4 -c:v libx264 -profile:v baseline -level 3.0 -pix_fmt yuv420p video-whatsapp.mp4
</code></pre>
<h1 id="heading-convert-a-funny-gif-to-shareable-friendly-mp4-format">Convert a funny gif to shareable friendly MP4 format.</h1>
<p>First, loop the GIF a few times so the video plays and people can enjoy your meme! Then convert that bigger GIF to an MP4.</p>
<pre><code class="lang-plaintext">ffmpeg.exe -stream_loop 3 -i .\funny.gif funny-loop.gif
ffmpeg.exe -i funny-loop.gif -f gif funny-loop-whatsapp.mp4
ffmpeg.exe -i funny-loop-whatsapp.mp4 -c:v libx264 -profile:v baseline -level 3.0 -pix_fmt yuv420p output.mp4
</code></pre>
<h1 id="heading-convert-video-to-x265-for-archiving-same-resolution">Convert video to x265 for archiving (same resolution).</h1>
<pre><code class="lang-plaintext">ffmpeg.exe -i myvideo.mp4 -c:v libx265 -vtag hvc1 -c:a copy myvideo-x265.mp4
</code></pre>
<h1 id="heading-trim-video-and-grab-specific-timestamps">Trim video and grab specific timestamps.</h1>
<pre><code class="lang-plaintext">ffmpeg.exe -ss 00:00:10 -t 10 -i '.\Lord of war [K1HEibypsTY].webm' trim.mp4
</code></pre>
<h1 id="heading-mix-audio-on-to-video">Mix audio on to video.</h1>
<p>Here my audio is larger than my video, and I want to mix the audio in with the video and cut it off. -shortest will use whichever is shorted, audio or video.</p>
<pre><code class="lang-plaintext">ffmpeg.exe -i .\trim.mp4 -i audio.mp3 -c copy -map 0:v:0 -map 1:a:0 -shortest mix.mp4
</code></pre>
]]></content:encoded></item><item><title><![CDATA[How to install Postgres 13 in Linux Mint 20.2]]></title><description><![CDATA[Hey gang - here's how you can easily install Postgres 13 on your shiny new Linux Mint 20 computer.
Add the deb url to your sources list
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main" > \      
/etc/apt/sources.list.d/...]]></description><link>https://sergiotapia.com/how-to-install-postgres-13-in-linux-mint-202</link><guid isPermaLink="true">https://sergiotapia.com/how-to-install-postgres-13-in-linux-mint-202</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Linux]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Fri, 12 Nov 2021 21:57:24 GMT</pubDate><content:encoded><![CDATA[<p>Hey gang - here's how you can easily install Postgres 13 on your shiny new Linux Mint 20 computer.</p>
<h1 id="add-the-deb-url-to-your-sources-list">Add the deb url to your sources list</h1>
<pre><code>sudo sh -<span class="hljs-built_in">c</span> 'echo <span class="hljs-string">"deb http://apt.postgresql.org/pub/repos/apt/ focal-pgdg main"</span> &gt; \      
/etc/apt/sources.list.d/postgresql.list'
</code></pre><h1 id="add-postgres-official-key">Add postgres' official key</h1>
<pre><code><span class="hljs-attribute">wget</span> --quiet -O - https://www.postgresql.org/media/keys/ACCC<span class="hljs-number">4</span>CF<span class="hljs-number">8</span>.asc | sudo apt-key add -
</code></pre><h1 id="update-and-upgrade-apt">Update and upgrade apt</h1>
<pre><code>sudo apt <span class="hljs-keyword">update</span>
sudo apt <span class="hljs-keyword">upgrade</span>
</code></pre><h1 id="install-postgresql-13">Install postgresql-13</h1>
<pre><code><span class="hljs-attribute">sudo</span> apt-get install postgresql-<span class="hljs-number">13</span> postgresql-client-<span class="hljs-number">13</span>
</code></pre><h1 id="run-the-postgres-13-service">Run the postgres 13 service</h1>
<pre><code><span class="hljs-attribute">sudo</span> systemctl status postgresql
</code></pre><h1 id="verify-you-have-everything-installed">Verify you have everything installed</h1>
<pre><code><span class="hljs-attribute">psql</span> --version                                                              
<span class="hljs-attribute">psql</span> (PostgreSQL) <span class="hljs-number">13</span>.<span class="hljs-number">5</span> (Ubuntu <span class="hljs-number">13</span>.<span class="hljs-number">5</span>-<span class="hljs-number">1</span>.pgdg<span class="hljs-number">20</span>.<span class="hljs-number">04</span>+<span class="hljs-number">1</span>)
</code></pre><p>You're done!</p>
]]></content:encoded></item><item><title><![CDATA[How to use custom adblock rules in Brave Browser 1.31 and up.]]></title><description><![CDATA[You're watching a cool video on youtube and these things appear at the end of the video, blocking important information.

Here's how to easily get rid of them in Brave Browser.
Type in brave://adblock/ in the address bar and hit .
Scroll all the way ...]]></description><link>https://sergiotapia.com/how-to-use-custom-adblock-rules-in-brave-browser-131-and-up</link><guid isPermaLink="true">https://sergiotapia.com/how-to-use-custom-adblock-rules-in-brave-browser-131-and-up</guid><category><![CDATA[Browsers]]></category><category><![CDATA[Google Chrome]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Fri, 12 Nov 2021 04:53:30 GMT</pubDate><content:encoded><![CDATA[<p>You're watching a cool video on youtube and these things appear at the end of the video, blocking important information.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636692640544/8ihJHJxI9.png" alt="image.png" /></p>
<p>Here's how to easily get rid of them in Brave Browser.</p>
<p>Type in <code>brave://adblock/</code> in the address bar and hit .</p>
<p>Scroll all the way down to find the custom ruleset input. Here's you can add the rule to block these annoying youtube end screen blocks.</p>
<pre><code><span class="hljs-selector-tag">youtube</span><span class="hljs-selector-class">.com</span>##<span class="hljs-selector-class">.ytp-ce-element</span>
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636692713753/YQ9OlT0ao.png" alt="image.png" /></p>
<p>There is no save button so click out of the input somewhere and wait a bit then refresh to confirm the rule was saved.</p>
<p>No more annoying youtube end screen boxes!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636692772094/BzXtLTAYy.png" alt="image.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Integrating AppSignal with Phoenix Liveview tutorial]]></title><description><![CDATA[Right, you want to use Appsignal to monitor your Phoenix Liveview application? Here's how you do it.
Note: This guide assumes you're on Liveview 0.17.5 or newer because we depend on the new liveview event hooks.
First install the appsignal_phoenix pa...]]></description><link>https://sergiotapia.com/integrating-appsignal-with-phoenix-liveview-tutorial</link><guid isPermaLink="true">https://sergiotapia.com/integrating-appsignal-with-phoenix-liveview-tutorial</guid><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Elixir]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Thu, 11 Nov 2021 20:16:27 GMT</pubDate><content:encoded><![CDATA[<p>Right, you want to use Appsignal to monitor your Phoenix Liveview application? Here's how you do it.</p>
<p><strong>Note: This guide assumes you're on Liveview 0.17.5 or newer because we depend on the new liveview event hooks.</strong></p>
<p>First install the <code>appsignal_phoenix</code> package. Because this package already depends on the <code>appsignal</code> package, there's no need for you to manually add it.</p>
<pre><code>defp deps <span class="hljs-keyword">do</span>
  [
    {<span class="hljs-symbol">:appsignal_phoenix</span>, <span class="hljs-string">"~&gt; 2.0"</span>},
</code></pre><p>Then we're going to hook into the lifecycle in two spots. One for the <code>mount</code> event, and one for the <code>handle_event</code>.</p>
<p>In your <code>myproject_web.ex</code> add two <code>on_mount</code> handlers.</p>
<pre><code>  def live_view <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">quote</span> <span class="hljs-keyword">do</span>
      use Phoenix.LiveView,
        layout: {GamingWeb.LayoutView, "live.html"}

      on_mount(GamingWeb.InitAssigns)
      on_mount(GamingWeb.Appsignal)

      unquote(view_helpers())
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
</code></pre><h1 id="tracking-mount-events">Tracking mount events</h1>
<p>Let's start by creating the <code>InitAssigns</code> module.</p>
<pre><code>defmodule GamingWeb.InitAssigns <span class="hljs-keyword">do</span>
  @moduledoc <span class="hljs-keyword">false</span>
  <span class="hljs-keyword">import</span> Phoenix.LiveView
  <span class="hljs-keyword">import</span> Appsignal.Phoenix.LiveView, <span class="hljs-keyword">only</span>: [live_view_action: <span class="hljs-number">5</span>]

  def on_mount(:<span class="hljs-keyword">default</span>, params, <span class="hljs-keyword">session</span>, socket) <span class="hljs-keyword">do</span>
    # her<span class="hljs-string">e's where we'</span>re calling appsignal instrumentation
    live_view_action(socket.<span class="hljs-keyword">view</span>, "mount", params, socket, fn -&gt;
      socket =
        socket
        |&gt; assign_new(:<span class="hljs-built_in">current_user</span>, fn -&gt;
          get_user(<span class="hljs-keyword">session</span>["user_token"])
        <span class="hljs-keyword">end</span>)

      {:cont, socket}
    <span class="hljs-keyword">end</span>)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre><p>We're done, every <code>mount</code> will be tracked properly on Appsignal.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636661510757/iAhNI-RNU.png" alt="image.png" /></p>
<h1 id="tracking-handleevent-events">Tracking handle_event events</h1>
<p>We're going to track the <code>handle_event</code> lifecycle event. If you want you can track others like: </p>
<pre><code>defmodule GamingWeb.Appsignal <span class="hljs-keyword">do</span>
  @moduledoc <span class="hljs-literal">false</span>
  import Phoenix.LiveView
  import Appsignal.Phoenix.LiveView, <span class="hljs-symbol">only:</span> [<span class="hljs-symbol">live_view_action:</span> <span class="hljs-number">5</span>]

  <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">on_mount</span><span class="hljs-params">(<span class="hljs-symbol">:default</span>, _params, _session, socket)</span></span> <span class="hljs-keyword">do</span>
    socket =
      attach_hook(socket, <span class="hljs-symbol">:mount_hook</span>, <span class="hljs-symbol">:handle_event</span>, fn
        event, params, socket -&gt;
          live_view_action(socket.view, event, params, socket, fn -&gt;
            {<span class="hljs-symbol">:cont</span>, socket}
          <span class="hljs-keyword">end</span>)
      <span class="hljs-keyword">end</span>)

    {<span class="hljs-symbol">:cont</span>, socket}
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre><p>And we're done. Here I'm tracking a simple form event named <code>submit</code>. </p>
<pre><code><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">handle_event</span><span class="hljs-params">(<span class="hljs-string">"submit"</span>, <span class="hljs-string">%{"waitlist" =&gt; params}</span>, socket)</span></span> <span class="hljs-keyword">do</span>
  {<span class="hljs-symbol">:noreply</span>, socket}
<span class="hljs-keyword">end</span>
</code></pre><p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1636661642416/pBD0P0Y28.png" alt="image.png" /></p>
]]></content:encoded></item><item><title><![CDATA[Your interview process sucks, you aren't FAANGM.]]></title><description><![CDATA[Your interview process is terrible and I'm going to show you how to fix it.
Do you know why FAANGM companies have these very complicated interview processes? Because they get literally thousands of applicants for a position. 
They need some kind of w...]]></description><link>https://sergiotapia.com/your-interview-process-sucks-you-arent-faangm</link><guid isPermaLink="true">https://sergiotapia.com/your-interview-process-sucks-you-arent-faangm</guid><category><![CDATA[interview]]></category><category><![CDATA[Technical interview]]></category><category><![CDATA[Career]]></category><category><![CDATA[hiring]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Thu, 14 Oct 2021 19:44:11 GMT</pubDate><content:encoded><![CDATA[<h1 id="your-interview-process-is-terrible-and-im-going-to-show-you-how-to-fix-it">Your interview process is terrible and I'm going to show you how to fix it.</h1>
<p>Do you know why FAANGM companies have these very complicated interview processes? Because they get literally <strong>thousands</strong> of applicants for a position. </p>
<p>They need some kind of way to cull 99% of applications. What's the easiest way to do that? Leetcode/hackerrank type questions. These companies unceremoniously reject thousands of engineers and whoever is left at the end gets to experience the real interview process. The one where people actually listen to you and ask about your experience.</p>
<p>Your company is not FAANGM. You do not get thousands of applicants. You are not scaling to two-hundred million users across the globe.</p>
<p>You are missing out on great candidates.</p>
<h1 id="how-do-i-fix-it-how-do-i-find-talented-people">How do I fix it? How do I find talented people?</h1>
<p>It's really simple and the results are great. Here's my current interview process:</p>
<ol>
<li>First round 30 minute interview with me, the VP of Engineering. <ul>
<li>We talk about your previous experience.</li>
<li>Career goals, what you want to work on.</li>
<li>I see if it's a good fit for the problems I need solved.</li>
</ul>
</li>
<li><a target="_blank" href="https://github.com/play-oxygen/takehome-fullstack-engineer">Take-home assignment</a>  that takes about 5 hours to complete.<ul>
<li>This is a real project with a proper <code>README.md</code> to get it running locally. </li>
<li>Since we work with real-time data and data ingestion, our take-home reflects what you will be doing all day. No leetcode. Just good old fashioned: "can you do what we need you to do?"</li>
<li>Talk to an API, render results in real-time, allow users to favorite a line-item. Focused.</li>
</ul>
</li>
<li>Screenshare for one hour to review your take-home.<ul>
<li>You, one of our engineers and I sit down and go through the three tasks we asked you to complete, and you talk about your solution. </li>
<li>Very casual, low stress. </li>
<li>We poke holes, discuss alternative approaches and get a feel for what it would be like to work with you.</li>
</ul>
</li>
</ol>
<p>There is no step 4.</p>
<h1 id="this-only-works-if-you-have-engineers-on-your-team">This only works if you have engineers on your team</h1>
<p>If your founding team is non-technical, you cannot take this approach. In these cases, I recommend using a service like <a target="_blank" href="https://karat.com/">Karat</a> I have taken a Karat interview once and the tool was serviceable, and process was technically sound.</p>
<p>But if you do have engineers on your team, give out take-home assignments. Give people a chance to show you what they are capable of and you will find the talent you need. </p>
<p>Being able to find rectangles in a grid of 0's and 1's doesn't mean that person will be able to deliver a good product and find product-market fit.</p>
<h1 id="would-you-like-to-experience-our-interview-process-im-hiring">Would you like to experience our interview process? I'm hiring!</h1>
<p>https://docs.google.com/document/d/1Gu3l1J8zh56MOCp-jpzLGbifOIHb2iGbX31qikLI384/edit?usp=sharing</p>
<p>Apply now and help us find awesome product-market fit in the esports gaming industry.</p>
]]></content:encoded></item><item><title><![CDATA[How to use multiple Github accounts with ssh keys]]></title><description><![CDATA[I have my dev box and use it to write both personal projects and work related code.
Here's how to automatically use different ssh keys for different projects on the same machine.

Create two different ssh keys
Make sure you add your public keys to ea...]]></description><link>https://sergiotapia.com/how-to-use-multiple-github-accounts-with-ssh-keys</link><guid isPermaLink="true">https://sergiotapia.com/how-to-use-multiple-github-accounts-with-ssh-keys</guid><category><![CDATA[GitHub]]></category><category><![CDATA[ssh]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Tue, 24 Aug 2021 17:47:00 GMT</pubDate><content:encoded><![CDATA[<p>I have my dev box and use it to write both personal projects and work related code.</p>
<p>Here's how to automatically use different ssh keys for different projects on the same machine.</p>
<hr />
<h1 id="create-two-different-ssh-keys">Create two different ssh keys</h1>
<p>Make sure you add your public keys to each of your github accounts. Pretty straightforward.</p>
<h1 id="create-a-config-file-in-ssh">Create a <code>config</code> file in <code>~/.ssh</code></h1>
<p>Include this:</p>
<pre><code><span class="hljs-comment"># My github username is sergiotapia</span>
<span class="hljs-string">Host</span> <span class="hljs-string">github.com-sergiotapia</span>
    <span class="hljs-string">HostName</span> <span class="hljs-string">github.com</span>
    <span class="hljs-string">User</span> <span class="hljs-string">git</span>
    <span class="hljs-string">IdentityFile</span> <span class="hljs-string">~/.ssh/mypersonalidentity</span>
        <span class="hljs-string">IdentitiesOnly</span> <span class="hljs-literal">yes</span>        

<span class="hljs-comment"># Assume my organization is called mycompany</span>
<span class="hljs-string">Host</span> <span class="hljs-string">github.com-mycompany</span>
    <span class="hljs-string">HostName</span> <span class="hljs-string">github.com</span>
    <span class="hljs-string">User</span> <span class="hljs-string">git</span>
    <span class="hljs-string">IdentityFile</span> <span class="hljs-string">~/.ssh/myworkidentity</span>
        <span class="hljs-string">IdentitiesOnly</span> <span class="hljs-literal">yes</span>
</code></pre><h1 id="configuring-your-git-remotes-to-properly-use-the-right-key">Configuring your git remotes to properly use the right key</h1>
<p>There's two ways for you to go about this.</p>
<h2 id="for-new-projects-you-need-to-clone">For new projects you need to clone:</h2>
<p>Clone the project using <code>github.com-mycompany</code> or <code>github.com-sergiotapia</code>.</p>
<pre><code><span class="hljs-attribute">git</span> clone git<span class="hljs-variable">@github</span>.com-mycompany:mycompany/backend-api.git

<span class="hljs-comment"># or your personal account</span>

git clone git<span class="hljs-variable">@github</span>.com-sergiotapia:sergiotapia/backend-api.git
</code></pre><h2 id="for-existing-projects-you-have-locally-but-need-to-work-on">For existing projects you have locally but need to work on.</h2>
<p>Set the git remote using the proper naming structure.</p>
<pre><code>git remote <span class="hljs-keyword">remove</span> origin
git remote <span class="hljs-keyword">add</span> origin git@github.com-mycompany:mycompany/backend-api.git

<span class="hljs-meta"># or your personal account</span>

git remote <span class="hljs-keyword">remove</span> origin
git remote <span class="hljs-keyword">add</span> origin git@github.com-sergiotapia:sergiotapia/backend-api.git
</code></pre>]]></content:encoded></item><item><title><![CDATA[Phoenix 1.6.0 LiveView + esbuild + Tailwind JIT + AlpineJS - A brief tutorial.]]></title><description><![CDATA[Phoenix 1.6.0 is nearing release, and when it comes out it will ditch Webpack for esbuild. It's a smaller integration, more predictable and just more productive.
There's very little material out there describing how to set up a liveview, tailwind jit...]]></description><link>https://sergiotapia.com/phoenix-160-liveview-esbuild-tailwind-jit-alpinejs-a-brief-tutorial</link><guid isPermaLink="true">https://sergiotapia.com/phoenix-160-liveview-esbuild-tailwind-jit-alpinejs-a-brief-tutorial</guid><category><![CDATA[Phoenix framework]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Elixir]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Sun, 15 Aug 2021 01:02:57 GMT</pubDate><content:encoded><![CDATA[<p>Phoenix 1.6.0 is nearing release, and when it comes out it will ditch Webpack for esbuild. It's a smaller integration, more predictable and just more productive.</p>
<p>There's very little material out there describing how to set up a liveview, tailwind jit and alpine project using Phoenix 1.6.0</p>
<p>Here's a little guide I pieced together out of forum posts, tweets, github issues and IRC. I can't thank everyone who helped me enough, they did the work, I just compiled it here for posterity.</p>
<p>If you're impatient just look at the code commits, it's this article step by step.</p>
<p>https://github.com/sergiotapia/golden</p>
<blockquote>
<p><strong>Update #1, November 11, 2021:</strong></p>
<p>Alpine ^3.5.0 now targets es2017 so make sure you update <code>config/config.exs</code>. Check the last commit in the repo for a quick fix.</p>
</blockquote>
<hr />
<blockquote>
<p>At the time of writing, Phoenix 1.6.0-rc.0 is out! https://www.phoenixframework.org/blog/phoenix-1.6-released</p>
</blockquote>
<p>Create your project.</p>
<pre><code>mix phx.<span class="hljs-built_in">new</span> golden <span class="hljs-comment">--live</span>
</code></pre><p>In the <code>assets</code> folder, install dev dependencies, also install alpinejs as a prod dependency.</p>
<pre><code>cd assets/
npm <span class="hljs-keyword">install</span> autoprefixer postcss postcss-<span class="hljs-keyword">import</span> postcss-cli tailwindcss <span class="hljs-comment">--save-dev</span>
npm <span class="hljs-keyword">install</span> alpinejs
</code></pre><h1 id="configuring-the-javascript-pipeline">Configuring the Javascript pipeline.</h1>
<p>We use esbuild to build our javascript payload.</p>
<p>Make sure you comment out line 3 of <code>app.js</code>, if you don't esbuild will also compile your css and will thrash any CSS pipelines you set up for tailwind jit. We will use postcss for CSS building later in this article.</p>
<pre><code>// We <span class="hljs-keyword">import</span> the CSS which <span class="hljs-keyword">is</span> extracted <span class="hljs-keyword">to</span> its own file <span class="hljs-keyword">by</span> esbuild.
// Remove this <span class="hljs-type">line</span> <span class="hljs-keyword">if</span> you <span class="hljs-keyword">add</span> a your own CSS build pipeline (e.g postcss).
// <span class="hljs-keyword">import</span> "../css/app.css"
</code></pre><p>Add alpinejs to your <code>app.js</code> file and make sure you configure the livesocket to call alpine on dom updates.</p>
<pre><code><span class="hljs-comment">// import Alpine</span>
<span class="hljs-keyword">import</span> Alpine <span class="hljs-keyword">from</span> <span class="hljs-string">"alpinejs"</span>;

<span class="hljs-comment">// Add this before your liveSocket call.</span>
<span class="hljs-built_in">window</span>.Alpine = Alpine;
Alpine.start();

---
<span class="hljs-comment">// Add dom update support for Alpine.</span>
<span class="hljs-comment">// before:</span>
<span class="hljs-keyword">let</span> liveSocket = <span class="hljs-keyword">new</span> LiveSocket(<span class="hljs-string">"/live"</span>, Socket, {<span class="hljs-attr">params</span>: {<span class="hljs-attr">_csrf_token</span>: csrfToken}})

<span class="hljs-comment">// after:</span>
<span class="hljs-keyword">let</span> hooks = {};
<span class="hljs-keyword">let</span> liveSocket = <span class="hljs-keyword">new</span> LiveSocket(<span class="hljs-string">"/live"</span>, Socket, {
  <span class="hljs-attr">params</span>: { <span class="hljs-attr">_csrf_token</span>: csrfToken },
  <span class="hljs-attr">hooks</span>: hooks,
  <span class="hljs-attr">dom</span>: {
    onBeforeElUpdated(<span class="hljs-keyword">from</span>, to) {
      <span class="hljs-keyword">if</span> (<span class="hljs-keyword">from</span>._x_dataStack) {
        <span class="hljs-built_in">window</span>.Alpine.clone(<span class="hljs-keyword">from</span>, to);
      }
    },
  },
});
</code></pre><h1 id="configuring-the-css-pipeline">Configuring the CSS pipeline</h1>
<p>This one is pretty complicated but bear with me.</p>
<p>Create <code>postcss.config.js</code> file in <code>assets</code> folder.</p>
<pre><code><span class="hljs-string">module.exports</span> <span class="hljs-string">=</span> {
  <span class="hljs-attr">plugins:</span> {
    <span class="hljs-attr">'postcss-import':</span> {},
    <span class="hljs-attr">tailwindcss:</span> {},
    <span class="hljs-attr">autoprefixer:</span> {},
  }
}
</code></pre><p>Create <code>tailwind.config.js</code> file in <code>assets</code> folder.</p>
<pre><code><span class="hljs-string">module.exports</span> <span class="hljs-string">=</span> {
  <span class="hljs-attr">mode:</span> <span class="hljs-string">"jit"</span>,
  <span class="hljs-attr">purge:</span> [<span class="hljs-string">"./js/**/*.js"</span>, <span class="hljs-string">"../lib/*_web/**/*.*ex"</span>],
  <span class="hljs-attr">theme:</span> {
    <span class="hljs-attr">extend:</span> {},
  },
  <span class="hljs-attr">variants:</span> {
    <span class="hljs-attr">extend:</span> {},
  },
  <span class="hljs-attr">plugins:</span> [],
}<span class="hljs-string">;</span>
</code></pre><p>Add Tailwind's basic css imports to the top of your <code>app.css</code> file.</p>
<pre><code><span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/base"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/components"</span>;
<span class="hljs-keyword">@import</span> <span class="hljs-string">"tailwindcss/utilities"</span>;
</code></pre><p>Finally, add a watcher to <code>dev.exs</code> so your compiled <code>app.css</code> is automatically reloaded as you work on your project.</p>
<pre><code><span class="hljs-comment"># dev.exs should ultimately end up looking like this:</span>
<span class="hljs-string">config</span> <span class="hljs-string">:golden,</span> <span class="hljs-string">GoldenWeb.Endpoint,</span>
  <span class="hljs-comment"># Binding to loopback ipv4 address prevents access from other machines.</span>
  <span class="hljs-comment"># Change to `ip: {0, 0, 0, 0}` to allow access from other machines.</span>
  <span class="hljs-attr">http:</span> [<span class="hljs-attr">ip:</span> {<span class="hljs-number">127</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>}, <span class="hljs-attr">port:</span> <span class="hljs-number">4000</span>]<span class="hljs-string">,</span>
  <span class="hljs-attr">debug_errors:</span> <span class="hljs-literal">true</span><span class="hljs-string">,</span>
  <span class="hljs-attr">code_reloader:</span> <span class="hljs-literal">true</span><span class="hljs-string">,</span>
  <span class="hljs-attr">check_origin:</span> <span class="hljs-literal">false</span><span class="hljs-string">,</span>
  <span class="hljs-attr">watchers:</span> [
    <span class="hljs-comment"># Start the esbuild watcher by calling Esbuild.install_and_run(:default, args)</span>
    <span class="hljs-attr">esbuild:</span> {<span class="hljs-string">Esbuild</span>, <span class="hljs-string">:install_and_run</span>, [<span class="hljs-string">:default</span>, <span class="hljs-string">~w(--sourcemap=inline</span> <span class="hljs-string">--watch)</span>]},
    <span class="hljs-attr">npx:</span> [
      <span class="hljs-string">"tailwindcss"</span>,
      <span class="hljs-string">"--input=css/app.css"</span>,
      <span class="hljs-string">"--output=../priv/static/assets/app.css"</span>,
      <span class="hljs-string">"--postcss"</span>,
      <span class="hljs-string">"--watch"</span>,
      <span class="hljs-attr">cd:</span> <span class="hljs-string">Path.expand("../assets"</span>, <span class="hljs-string">__DIR__)</span>
    ]
  ]
</code></pre><h1 id="at-this-point-you-should-have-nice-working-development-environment">At this point you should have nice working development environment.</h1>
<p>Just run <code>mix phx.server</code> and visit https://localhost:4000</p>
<p>Let's figure out deployment to production next.</p>
<p>Create a deploy script in <code>package.json</code></p>
<pre><code><span class="hljs-string">"scripts"</span>: {
    <span class="hljs-string">"deploy"</span>: <span class="hljs-string">"NODE_ENV=production postcss css/app.css -o ../priv/static/assets/app.css"</span>
  },
</code></pre><p>Modify the <code>assets.deploy</code> alias in <code>mix.exs</code></p>
<pre><code>defp aliases <span class="hljs-keyword">do</span>
    [
      setup: [<span class="hljs-string">"deps.get"</span>, <span class="hljs-string">"ecto.setup"</span>, <span class="hljs-string">"cmd --cd assets npm install"</span>],
      <span class="hljs-string">"ecto.setup"</span>: [<span class="hljs-string">"ecto.create"</span>, <span class="hljs-string">"ecto.migrate"</span>, <span class="hljs-string">"run priv/repo/seeds.exs"</span>],
      <span class="hljs-string">"ecto.reset"</span>: [<span class="hljs-string">"ecto.drop"</span>, <span class="hljs-string">"ecto.setup"</span>],
      <span class="hljs-keyword">test</span>: [<span class="hljs-string">"ecto.create --quiet"</span>, <span class="hljs-string">"ecto.migrate --quiet"</span>, <span class="hljs-string">"test"</span>],
      <span class="hljs-string">"assets.deploy"</span>: [
        <span class="hljs-string">"cmd --cd assets npm run deploy"</span>,
        <span class="hljs-string">"esbuild default --minify"</span>,
        <span class="hljs-string">"phx.digest"</span>
      ]
    ]
  <span class="hljs-keyword">end</span>
</code></pre><p>Finally let's build a <code>build.sh</code> at root folder deploy script. This will build our app using Elixir releases in <code>prod</code> MIX_ENV and digest our static js and css assets.</p>
<pre><code><span class="hljs-meta">#!/usr/bin/env bash</span>
<span class="hljs-comment"># exit on error</span>
<span class="hljs-built_in">set</span> -o errexit

<span class="hljs-comment"># Install deps</span>
npm install --prefix ./assets
mix deps.get --only prod

<span class="hljs-comment"># Initial setup</span>
MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix compile

<span class="hljs-comment"># Migrate the database</span>
MIX_ENV=prod mix ecto.migrate

<span class="hljs-comment"># Build the release and overwrite the existing release directory</span>
MIX_ENV=prod mix release --overwrite
</code></pre><p>Give that script run permissions.</p>
<pre><code><span class="hljs-keyword">chmod</span> a+<span class="hljs-keyword">x</span> build.sh
</code></pre><p>Run the build script.</p>
<pre><code>./build.sh
</code></pre><p><strong>Note: This command WILL FAIL because you need to set up a database url environment variable for prod MIX_ENV. Set that up using your database of choosing.</strong></p>
]]></content:encoded></item><item><title><![CDATA[Taking a company to Series C as VP of Engineering.]]></title><description><![CDATA[I'm Sergio, VP of Engineering at a great startup headquartered in Miami called  Papa.
I joined two years ago as the first engineering hire. My role was Director of Engineering. The technical team at the time was an offshore engineering firm with a sp...]]></description><link>https://sergiotapia.com/taking-a-company-to-series-c-as-vp-of-engineering</link><guid isPermaLink="true">https://sergiotapia.com/taking-a-company-to-series-c-as-vp-of-engineering</guid><category><![CDATA[management]]></category><category><![CDATA[cto]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Mon, 09 Aug 2021 03:51:24 GMT</pubDate><content:encoded><![CDATA[<p>I'm Sergio, VP of Engineering at a great startup headquartered in Miami called  <a target="_blank" href="https://www.papa.co/">Papa</a>.</p>
<p>I joined two years ago as the first engineering hire. My role was Director of Engineering. The technical team at the time was an offshore engineering firm with a specialty for Elixir work. The co-founders, Andrew and Alfredo, wanted someone local to Miami, experienced in leading engineering teams, and experienced with our tech stack.</p>
<p>I'm writing this article because there is such a draught of content for engineering leadership in that "growth" phase. All I've seen is "CTO" articles from people leading a 2 person team, or articles from an experienced CTO that is leading a public 600 person engineering team. No real in-betweens.</p>
<p>Here are some of the lessons I learned leading Papa's engineering organization from Series A all the way to Series C. What I learned when our engineering team grew to 2 people, 4 people, 10 people, 25 people, 55 people. </p>
<p>This is the start of what I like to call "Growth CTO" lessons.</p>
<hr />
<h1 id="2-10-engineers">2-10 Engineers</h1>
<h2 id="velocity-and-one-goal">velocity and one goal</h2>
<p>Our tech stack has always been very modern and included things like Elixir, Graphql, and React Native.</p>
<p>The first thing I did when I joined was soaking in the technical choices that were made by the outsourcing firm. A lot of the choices made were great, and some had to be changed more to my liking.</p>
<p>As I hired our first 10 engineers, my responsibilities transitioned more to people management. I still spent most of my time coding, but right around hire #5, I noticed more time spent on planning and building the engineering culture. A lot of time spent talking about our products, how our business functions, how we make money, who are our customers -- I wanted my engineers to understand this well so they knew who they were building these products for and why it was important.</p>
<p><strong>This is when you want to set your org foundations.</strong></p>
<p>As an engineering org, what do you value most? Some teams care about quality, others care about velocity, others care about adhering to a process.</p>
<p>Whatever you choose, make sure to make it explicit and repeat it over and over to your team. Something new managers do not realize until it's too late is that what you say seldom sticks the first few times you say it. Whatever you value most from your team, put it in the engineering handbook and bring it up often during all-hands. Make people realize you care about this first and foremost and you are judging performance based on it.</p>
<p>I set up 1-on-1s, sprint planning sessions, and daily standups with the engineering team. I had the engineers reporting directly to me but we didn't really have skip-level reporting structures set up.</p>
<h1 id="11-30-engineers">11-30 Engineers</h1>
<h2 id="no-more-coding-much-more-teamwork">no more coding, much more teamwork</h2>
<p>We started growing very rapidly, making deals with really great customers and I found myself working with thirty engineers. Suddenly coordinating work between team members was much more difficult. </p>
<p>We used the "Spotify" method to organize our product teams. Each team had a proper mixture of backend/mobile/web/design and they focused on one product. We called them a "Papa Pod".</p>
<p>At this stage, I was promoted to VP of Engineering and I promoted my most senior backend engineer to Director of Engineering. As I hired more people into our teams, our engineers with longer tenure began asking about promotions and career opportunities at our organization. Understandably, they were concerned that I was hiring a lot of very experienced engineers.</p>
<p>My tip to you reader is to get a career ladder on paper as soon as you close your Series B. Work with your HR department, look online (I'm going to write an article about this in the future), or ask around your personal network. <strong>You may notice one person's stress, but a team hides stress very well.</strong></p>
<p>I set up clearer reporting lines. We had people who enjoyed doing player-coach type work with at most 6 direct reports. I had my 1-on-1s with the coaches and executive team. I did very little coding at this stage. I focused entirely on our culture, optimizing our delivery process, unblocking teams when they needed assistance, recruiting, and onboarding. </p>
<p>The decisions you made earlier in the company's life start to take automatic shape here and both good and bad are amplified.</p>
<h1 id="31-55-engineers">31-55 Engineers</h1>
<h2 id="building-a-strong-team-culture">Building a strong team culture</h2>
<p>When we hit the 50th Engineer milestone, almost all of my time was spent focused on management and training other engineering managers. I hired a Director of Engineering QA as well. Every decision I made at the start of this journey <strong>really</strong> started playing out before my eyes. All the training you gave your managers really pays off at this size. Keep your ears open, it's very easy to not know if bad things are happening within your org at this size. Have bi-weekly engineering all-hands to talk with your entire team about the company, the team, the work being done.</p>
<p>Platform team! I found people within the org that had a knack and desire for this kind of work and they began working on the infrastructure behind our software. Early discussions about service-oriented architecture, as our monolith setup was becoming very complex to maintain and ship.</p>
<p>The love I have for this job now comes from getting other people big wins. I spend time unblocking our product teams, empowering my engineering teams, and comforting people who have worked so hard for us and making sure their needs are thoughtfully considered. </p>
<h1 id="60-150-engineers">60-150 Engineers</h1>
<p>What's next? We have grown tremendously fast, our clients love us and we love them. I've seen this company grow from 24 people to 450 people and somehow still maintain such a warm inviting culture. As the engineering org grows it will require a lot of deliberate effort to maintain our culture and scale our reporting lines.</p>
<p>This will be my primary responsibility.</p>
<hr />
<p>In my next article, I'll talk about how to identify player coaches for potential future management roles in your engineering organization.</p>
]]></content:encoded></item><item><title><![CDATA[How to parse a json array with Nim]]></title><description><![CDATA[Given the following JSON, how can you parse it into a nice array to work with in Nim?
[
    {
        "name":"Sergio",
        "favoriteMovie":{
            "title":"Taxi Driver",
            "releaseYear":1976
        }
    },
    {
        "name":"...]]></description><link>https://sergiotapia.com/how-to-parse-a-json-array-with-nim</link><guid isPermaLink="true">https://sergiotapia.com/how-to-parse-a-json-array-with-nim</guid><category><![CDATA[json]]></category><dc:creator><![CDATA[Sergio Tapia]]></dc:creator><pubDate>Mon, 09 Aug 2021 00:33:21 GMT</pubDate><content:encoded><![CDATA[<p>Given the following JSON, how can you parse it into a nice array to work with in Nim?</p>
<pre><code>[
    {
        <span class="hljs-attr">"name"</span>:<span class="hljs-string">"Sergio"</span>,
        <span class="hljs-attr">"favoriteMovie"</span>:{
            <span class="hljs-attr">"title"</span>:<span class="hljs-string">"Taxi Driver"</span>,
            <span class="hljs-attr">"releaseYear"</span>:<span class="hljs-number">1976</span>
        }
    },
    {
        <span class="hljs-attr">"name"</span>:<span class="hljs-string">"Daniel"</span>,
        <span class="hljs-attr">"favoriteMovie"</span>:{
            <span class="hljs-attr">"title"</span>:<span class="hljs-string">"Frozen"</span>,
            <span class="hljs-attr">"releaseYear"</span>:<span class="hljs-number">2013</span>
        }
    }
]
</code></pre><p>It’s pretty easy, first let’s start by creating the types to marshal the json into.</p>
<pre><code><span class="hljs-keyword">type</span>
  Movie* = <span class="hljs-keyword">object</span>
    title*: <span class="hljs-keyword">Option</span>[string]
    releaseYear*: <span class="hljs-keyword">Option</span>[<span class="hljs-type">int</span>]

<span class="hljs-keyword">type</span> 
  Person* = <span class="hljs-keyword">object</span>
    <span class="hljs-type">name</span>*: <span class="hljs-keyword">Option</span>[string]
    favoriteMovie*: <span class="hljs-keyword">Option</span>[Movie]
</code></pre><p>Then it’s just a matter of parsing the json and using the to function to marshal into a <code>seq[Person]</code>.</p>
<pre><code>import json
import strformat
import options

<span class="hljs-class"><span class="hljs-keyword">type</span>
  <span class="hljs-title">Movie</span></span>* = object
    title*: <span class="hljs-built_in">Option</span>[string]
    releaseYear*: <span class="hljs-built_in">Option</span>[int]

<span class="hljs-class"><span class="hljs-keyword">type</span> 
  <span class="hljs-title">Person</span></span>* = object
    name*: <span class="hljs-built_in">Option</span>[string]
    favoriteMovie*: <span class="hljs-built_in">Option</span>[Movie]

<span class="hljs-keyword">let</span> responseJson = <span class="hljs-string">""</span><span class="hljs-string">"
[
    {
        "</span>name<span class="hljs-string">":"</span>Sergio<span class="hljs-string">",
        "</span>favoriteMovie<span class="hljs-string">":{
            "</span>title<span class="hljs-string">":"</span>Taxi Drive<span class="hljs-string">r",
            "</span>releaseYea<span class="hljs-string">r":1976
        }
    },
    {
        "</span>name<span class="hljs-string">":"</span>Daniel<span class="hljs-string">",
        "</span>favoriteMovie<span class="hljs-string">":{
            "</span>title<span class="hljs-string">":"</span>Frozen<span class="hljs-string">",
            "</span>releaseYea<span class="hljs-string">r":2013
        }
    }
]
"</span><span class="hljs-string">""</span>

<span class="hljs-keyword">let</span> parsedJson = parseJson(responseJson)
<span class="hljs-keyword">let</span> list = parsedJson.to(seq[Person])
echo $list
</code></pre><p>The output you’ll see is as expected.</p>
<pre><code>@[(<span class="hljs-type">name</span>: <span class="hljs-keyword">Some</span>("Sergio"), favoriteMovie: <span class="hljs-keyword">Some</span>((title: <span class="hljs-keyword">Some</span>("Taxi Driver"), releaseYear: <span class="hljs-keyword">Some</span>(<span class="hljs-number">1976</span>)))), (<span class="hljs-type">name</span>: <span class="hljs-keyword">Some</span>("Daniel"), favoriteMovie: <span class="hljs-keyword">Some</span>((title: <span class="hljs-keyword">Some</span>("Frozen"), releaseYear: <span class="hljs-keyword">Some</span>(<span class="hljs-number">2013</span>))))]
</code></pre>]]></content:encoded></item></channel></rss>