<?xml version="1.0" encoding="utf-8" ?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>CodeJam</title>
  <subtitle>Hey, I’m Val, welcome to my blog!</subtitle>
  <link href="https://www.codejam.info/feed.xml" rel="self" />
  <link href="https://www.codejam.info/" />
  <id>https://www.codejam.info/</id>
  <updated>2026-02-12T01:40:14.580Z</updated>
  <author>
    <name>Val</name>
  </author>
  <entry>
    <title>Run Astro middleware in front of static pages (Cloudflare Workers)</title>
    <link href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html" />
    <id>https://www.codejam.info/2026/02/astro-middleware-static-pages.html</id>
    <updated>2026-02-11T08:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Astro allows to prerender pages as static assets, so everything is
compiled at build time and can be served super quick.</p>
<p>But also, Astro has the concept of a middleware, that allows to run
custom logic in front of every request, which can be handy for things
like auth, redirects, proxying and more.</p>
<p>The problem? <a href="https://github.com/withastro/roadmap/discussions/869">The middleware is not run for static pages</a>.</p>
<h2 id="why-run-the-middleware-on-static-pages" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#why-run-the-middleware-on-static-pages"><span>Why run the middleware on static pages?</span></a></h2>
<p>In many cases this is not a problem, because you may not often need to
run custom logic in front of static pages. After all if those pages
needed custom logic per request, they’d probably be dynamic.</p>
<p>Pages behind auth are most likely dynamic so the content can be
contextual to the logged-in user. As for redirects and proxies, they
usually make sense <em>when no actual page</em> matches the URL, and Astro runs
the middleware in both those cases.</p>
<p>However my use case is a bit different. I’d like to get a sense of what
pages are visited on my site, and I don’t like the idea of client-side
tracking, partly because of the privacy stigma, and partly because “you
can’t trust the client”.</p>
<h3 id="unnecessary-note-on-client-side-tracking" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#unnecessary-note-on-client-side-tracking"><span>Unnecessary note on client-side tracking</span></a></h3>
<p>At that point it seems that mostly everyone on the internet does
client-side tracking, including every business I ever worked with. Yet I
never encountered cases where the data got heavily manipulated.</p>
<p>The only anomalies I ever noticed are the occasional pen test with all
sorts of injection attempts, but those typically just get ignored at
ingestion because the data is broken, or at worst, it breaks queries
because corrupted data <em>was</em> ingested (read: data type mismatch,
hopefully we know how to avoid SQL injections by now).</p>
<p>However it seems that most people have something else to do than
programmatically sending well formatted but fake tracking data on public
endpoints for the mere pleasure of causing chaos for someone else.</p>
<p>Despite that, I’m still allergic to client-side tracking because <em>on
paper</em> this is all still possible.</p>
<h4 id="unnecessary-note-on-http-logs" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#unnecessary-note-on-http-logs"><span>Unnecessary note on HTTP logs</span></a></h4>
<p>You could say I can achieve all this with HTTP logs without bothering
with an edge worker, and you’d be 100% right. However,
Cloudflare <a href="https://developers.cloudflare.com/logs/logpush/#availability">only gives access to HTTP logs to Enterprise customers</a>
and this is not exactly an interesting option for me right now.</p>
<p>If not for that it would definitely be my favorite solution.</p>
<h2 id="running-backend-code-in-front-of-static-pages" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#running-backend-code-in-front-of-static-pages"><span>Running backend code in front of static pages</span></a></h2>
<p>Back to the topic. Sadly there’s no generic way I found to run the
Astro middleware in front of static pages. This means the solution is
gonna be dependent on your <em>adapter</em>. In my case, I’m using the
<a href="https://docs.astro.build/en/guides/integrations-guide/cloudflare/">Cloudflare adapter</a>.</p>
<p>We have two layers to deal with here. Out of the box the logic is as
follows:</p>
<ul>
<li>Cloudflare Workers:
<ul>
<li>If there’s a static asset that matches the URL, serve that.</li>
<li>Otherwise, run the Astro <code>worker.js</code>.</li>
</ul>
</li>
<li>Astro <code>worker.js</code>:
<ul>
<li>If there’s a static asset that matches the URL, serve that.</li>
<li>Otherwise, run the user-provided middleware.</li>
</ul>
</li>
</ul>
<h2 id="the-cloudflare-part" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#the-cloudflare-part"><span>The Cloudflare part</span></a></h2>
<p>In order to mitigate the <em>first</em> layer (Cloudflare), we need to
configure the runtime to run the worker code in front to some or all
static assets. This is done in <code>wrangler.jsonc</code> using the
<code>run_worker_first</code> directive
(<a href="https://developers.cloudflare.com/workers/static-assets/routing/worker-script/#run-your-worker-script-first">relevant</a>
<a href="https://developers.cloudflare.com/workers/static-assets/binding/#run_worker_first">docs</a>):</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;assets&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;binding&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ASSETS&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;directory&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;./dist&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;run_worker_first&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
      <span class="hljs-string">&quot;/&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;/en&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;/en/*&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;/fr&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-string">&quot;/fr/*&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>In this case I force the worker to run for the index, as well as <code>/en</code>,
<code>/fr</code> and anything under.</p>
<p>This means for other assets like JS/CSS/images, we still skip the
worker, but for the static pages I have that match those paths,
Cloudflare will run the edge worker.</p>
<h2 id="the-astro-part" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#the-astro-part"><span>The Astro part</span></a></h2>
<p>Now we have Cloudflare run the Astro worker in front of static pages,
but it’s still not enough, because the Astro Cloudflare adapter
<a href="https://github.com/withastro/astro/blob/8780ff2926d59ed196c70032d2ae274b8415655c/packages/integrations/cloudflare/src/utils/handler.ts#L53-L56">skips our middleware</a>
anyway when a static asset matches.</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">if</span> (app.<span class="hljs-property">manifest</span>.<span class="hljs-property">assets</span>.<span class="hljs-title function_">has</span>(requestPathname)) {
  <span class="hljs-keyword">return</span> env.<span class="hljs-property">ASSETS</span>.<span class="hljs-title function_">fetch</span>(request.<span class="hljs-property">url</span>.<span class="hljs-title function_">replace</span>(<span class="hljs-regexp">/\.html$/</span>, <span class="hljs-string">&#x27;&#x27;</span>))
}
</code></pre>
<p>In order to solve that, we can’t use the Astro middleware anymore.
Instead we need to configure a custom entry point for the worker. This
means we now control the top-level worker code and run our logic there,
regardless what the adapter decides to do.</p>
<p>This is done with <a href="https://docs.astro.build/en/guides/integrations-guide/cloudflare/#workerentrypoint"><code>workerEntryPoint</code> option</a>
in <code>astro.config.mjs</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;astro/config&#x27;</span>
<span class="hljs-keyword">import</span> cloudflare <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@astrojs/cloudflare&#x27;</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-title function_">defineConfig</span>({
  <span class="hljs-comment">// ...</span>

  <span class="hljs-attr">adapter</span>: <span class="hljs-title function_">cloudflare</span>({
    <span class="hljs-attr">workerEntryPoint</span>: {
      <span class="hljs-attr">path</span>: <span class="hljs-string">&#x27;src/worker.ts&#x27;</span>,
    },
  }),

  <span class="hljs-comment">// ...</span>
})
</code></pre>
<p>Where <code>src/worker.ts</code> is a custom Cloudflare Worker entry file
<a href="https://docs.astro.build/en/guides/integrations-guide/cloudflare/#creating-a-custom-cloudflare-worker-entry-file">as documented here</a>:</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">SSRManifest</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;astro&#x27;</span>
<span class="hljs-keyword">import</span> { <span class="hljs-title class_">App</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;astro/app&#x27;</span>

<span class="hljs-keyword">import</span> { handle } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;@astrojs/cloudflare/handler&#x27;</span>

<span class="hljs-keyword">type</span> <span class="hljs-title class_">Env</span> = {
  [<span class="hljs-attr">key</span>: <span class="hljs-built_in">string</span>]: <span class="hljs-built_in">unknown</span>
  <span class="hljs-attr">ASSETS</span>: {
    <span class="hljs-attr">fetch</span>: <span class="hljs-function">(<span class="hljs-params"><span class="hljs-attr">req</span>: <span class="hljs-title class_">Request</span> | <span class="hljs-built_in">string</span></span>) =&gt;</span> <span class="hljs-title class_">Promise</span>&lt;<span class="hljs-title class_">Response</span>&gt;
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">createExports</span>(<span class="hljs-params"><span class="hljs-attr">manifest</span>: <span class="hljs-title class_">SSRManifest</span></span>) {
  <span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> <span class="hljs-title class_">App</span>(manifest)

  <span class="hljs-keyword">const</span> <span class="hljs-attr">fetch</span>: <span class="hljs-title class_">ExportedHandlerFetchHandler</span>&lt;<span class="hljs-title class_">Env</span>&gt; = <span class="hljs-title function_">async</span> (request, env, ctx) =&gt; {
    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> <span class="hljs-title function_">URL</span>(request.<span class="hljs-property">url</span>)
    <span class="hljs-keyword">const</span> { pathname, search } = url

    <span class="hljs-comment">// Do anything before Astro handles the request</span>

    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-title function_">handle</span>(manifest, app, request, env, ctx)

    <span class="hljs-comment">// Do anything after</span>

    <span class="hljs-keyword">return</span> response
  }

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">default</span>: {
      fetch,
    } <span class="hljs-keyword">satisfies</span> <span class="hljs-title class_">ExportedHandler</span>&lt;<span class="hljs-title class_">Env</span>&gt;,
  }
}
</code></pre>
<h2 id="what-about-development" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#what-about-development"><span>What about development?</span></a></h2>
<p>The <code>workerEntryPoint</code> is great for production, but <code>astro dev</code> won’t
pick up on that. So if you need any of this logic to <em>also</em> run in
development, you need to abstract it and also include it in
<code>middleware.ts</code>.</p>
<p>This works fine even for static pages because Astro do run the
middleware when generating static pages, it just outputs a warning if
you try to access things like request headers.</p>
<p>In my case, I chose to move all my production logic in <code>worker.ts</code>, so I
don’t rely on the middleware whatsoever in production. I use a
conditional export like follows in order to keep the middleware only in
development, where it mimics what <code>worker.ts</code> otherwise does in
production.</p>
<pre><code class="hljs language-ts"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { <span class="hljs-title class_">MiddlewareHandler</span> } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;astro&#x27;</span>

<span class="hljs-keyword">const</span> <span class="hljs-attr">handler</span>: <span class="hljs-title class_">MiddlewareHandler</span> = <span class="hljs-title function_">async</span> (context, next) =&gt; {
  <span class="hljs-comment">// ...</span>
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> onRequest = <span class="hljs-keyword">import</span>.<span class="hljs-property">meta</span>.<span class="hljs-property">env</span>.<span class="hljs-property">DEV</span> ? handler : <span class="hljs-literal">undefined</span>
</code></pre>
<h2 id="wrapping-up" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/astro-middleware-static-pages.html#wrapping-up"><span>Wrapping up</span></a></h2>
<p>In short: configure <code>run_worker_first</code> on Cloudflare so it runs the
worker in front of static pages, then use a custom <code>workerEntryPoint</code>
with the Astro Cloudflare adapter so you get full control over the
worker, and can run code <em>outside of the middleware</em> (which does not
run for static pages).</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Cloudflare Workers choke on asdf .tool-versions</title>
    <link href="https://www.codejam.info/2026/02/cloudflare-workers-choke-asdf-tool-versions.html" />
    <id>https://www.codejam.info/2026/02/cloudflare-workers-choke-asdf-tool-versions.html</id>
    <updated>2026-02-10T08:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>So you have a project you want to deploy to Cloudflare Workers, and you
happen to have a <code>.tool-versions</code> file to describe your dependencies,
even just a simple one like:</p>
<pre><code class="hljs">nodejs 24.13.1
</code></pre>
<p>Then your Cloudflare deploy fails with:</p>
<pre><code class="hljs">Initializing build environment...
Success: Finished initializing build environment
Cloning repository...
Found a .tool-versions file in repository root. Installing dependencies.
Restoring from dependencies cache
Restoring from build output cache
Failed: error occurred while installing tools or dependencies
</code></pre>
<p>There’s a
<a href="https://community.cloudflare.com/t/undocumented-tool-versions-support/855532">couple</a>
<a href="https://community.cloudflare.com/t/cloudflare-workers-deploy-crashes-on-tool-versions/871072">threads</a>
on
<a href="https://community.cloudflare.com/t/how-to-disable-automatic-installs-of-tool-versions-and-package-json/570450">Cloudflare</a>
<a href="https://community.cloudflare.com/t/support-pnpm-in-tool-versions/645235">Community</a>
about this and similar <code>.tool-versions</code> issues. They get closed after 15
days without any answer, with the oldest one from 2023 and still no
solution to this day.</p>
<p>The fact Cloudflare Workers (and Cloudflare Pages) look at
<code>.tool-versions</code> is undocumented, it so happens that it chokes on even
the most basic <code>.tool-versions</code> possible, so it essentially means the
mere presence of this file in your project will break your build on
Cloudflare, without any way to turn off this behavior (like forcing
Cloudflare to ignore that file). As reported in <a href="https://community.cloudflare.com/t/how-to-disable-automatic-installs-of-tool-versions-and-package-json/570450">this issue</a>,
the <code>SKIP_DEPENDENCY_INSTALL</code> environment variable does <em>not</em> help with
this behavior.</p>
<h2 id="the-solution" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/cloudflare-workers-choke-asdf-tool-versions.html#the-solution"><span>The solution</span></a></h2>
<p>So what’s left? Well, I had to remove <code>.tool-versions</code> from my
repository.</p>
<p>Instead, I moved it to <code>.tool-versions.template</code>, and when setting up
the repo in any environment that <em>actually</em> supports <code>.tool-versions</code>, I
just <code>cp .tool-versions.template .tool-versions</code> (with <code>.tool-versions</code>
being in <code>.gitignore</code>).</p>
<h2 id="setting-the-correct-node-js-version-on-cloudflare" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/02/cloudflare-workers-choke-asdf-tool-versions.html#setting-the-correct-node-js-version-on-cloudflare"><span>Setting the correct Node.js version on Cloudflare</span></a></h2>
<p>As for Cloudflare, in order to set the proper versions, I’m using the
<code>NODE_VERSION</code> and <code>PNPM_VERSION</code> build environment variables. (Also
<code>NPM_VERSION</code> and <code>YARN_VERSION</code> depending on your package manager of
choice).</p>
<pre><code class="hljs">NODE_VERSION=24.13.1
PNPM_VERSION=10.29.2
</code></pre>
<p>This is, of course, also undocumented, but it works!</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Join the discussion on <a href="https://x.com/valeriangalliat/status/2021294413893574883">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>The next chapter</title>
    <link href="https://www.codejam.info/2026/01/the-next-chapter.html" />
    <id>https://www.codejam.info/2026/01/the-next-chapter.html</id>
    <updated>2026-01-25T08:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Just over two years since the <a href="https://www.codejam.info/2024/01/now.html">last update</a>. Time
flies, heh?</p>
<h2 id="squamish" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#squamish"><span>Squamish</span></a></h2>
<p>I’m well settled in Squamish now. Two years already!</p>
<p>As an introvert and working remote, it was a bit of a slow start
rebuilding social circles from scratch. It really takes being
intentional about it. <a href="https://www.facebook.com/groups/southcoasttouring/">Facebook</a>
<a href="https://www.facebook.com/groups/1322245267842175/">groups</a> and the
<a href="https://getoak.app/">Oak app</a> are pretty sweet, and believe it or not,
talking to strangers <abbr title="In real life">IRL</abbr> also works!
The good thing is that the more people you know, the more people you
meet, so it gets pretty effortless over time.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Shovelnose" title="Shovelnose" srcset="../../img/2026/01/now/touring/01.webp 2x" loading="lazy">
  <img alt="Decker" title="Decker" srcset="../../img/2026/01/now/touring/02.webp 2x" loading="lazy">
  <img alt="Snow pit" srcset="../../img/2026/01/now/touring/03.webp 2x" loading="lazy">
</figure>
<h2 id="mountains" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#mountains"><span>Mountains 🏔️</span></a></h2>
<p>In 2025 I’ve been out in the mountains like crazy, and hit 130,000
meters of elevation gain all sports combined (ski touring, trail
running, gravel and mountain biking). It’s my biggest elevation gain
year lifetime, 30,000 meters more than 2024. 💪</p>
<p>That includes scrambling Sky Pilot, climbing Star Check (3 times lol),
Cloudburst, Goat Ridge scramble, Blackcomb Peak ridge scramble, a
marathon to Castle Towers, Gentian Peak and Panorama Ridge, Ossa
Mountain, Rainbow Mountain, the Armchair Traverse (Mount Cook and Mount
Weart), Locomotive Mountain and its loop scramble, Coliseum Mountain,
and finishing with the Howe Sound Crest Trail (with extra Harvey,
Brunswick and West Lion lol get rekt). What a year!</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Fissile Peak" title="Fissile Peak" srcset="../../img/2026/01/now/mountains/01.webp 2x" loading="lazy">
  <img alt="Whistler Peak" title="Whistler Peak" srcset="../../img/2026/01/now/mountains/02.webp 2x" loading="lazy">
  <img alt="Ledge Mountain" title="Ledge Mountain" srcset="../../img/2026/01/now/mountains/03.webp 2x" loading="lazy">
  <img alt="Decker Mountain" title="Decker Mountain" srcset="../../img/2026/01/now/mountains/04.webp 2x" loading="lazy">
  <img alt="A whisky jack at Crystal Hut" title="A whisky jack at Crystal Hut" srcset="../../img/2026/01/now/mountains/05.webp 2x" loading="lazy">
  <img alt="Mount Cayley view from Brandywine" title="Mount Cayley view from Brandywine" srcset="../../img/2026/01/now/mountains/06.webp 2x" loading="lazy">
  <img alt="Intergalactic (Diamond Head)" title="Intergalactic (Diamond Head)" srcset="../../img/2026/01/now/mountains/07.webp 2x" loading="lazy">
  <img alt="Intergalactic (Diamond Head)" title="Intergalactic (Diamond Head)" srcset="../../img/2026/01/now/mountains/08.webp 2x" loading="lazy">
  <img alt="Sky Pilot and Copilot" title="Sky Pilot and Copilot" srcset="../../img/2026/01/now/mountains/09.webp 2x" loading="lazy">
  <img alt="Castle Towers" title="Castle Towers" srcset="../../img/2026/01/now/mountains/10.webp 2x" loading="lazy">
  <img alt="Garibaldi view from Helm Glacier saddle" title="Garibaldi view from Helm Glacier saddle" srcset="../../img/2026/01/now/mountains/11.webp 2x" loading="lazy">
  <img alt="Castle Towers summit" title="Castle Towers summit" srcset="../../img/2026/01/now/mountains/12.webp 2x" loading="lazy">
  <img alt="Castle Towers view from the summit" title="Castle Towers view from the summit" srcset="../../img/2026/01/now/mountains/13.webp 2x" loading="lazy">
  <img alt="Panorama Ridge and Black Tusk view from Castle Towers" title="Panorama Ridge and Black Tusk view from Castle Towers" srcset="../../img/2026/01/now/mountains/14.webp 2x" loading="lazy">
  <img alt="Pelion view from Ossa Mountain" title="Pelion view from Ossa Mountain" srcset="../../img/2026/01/now/mountains/15.webp 2x" loading="lazy">
  <img alt="Garibaldi Lake" title="Garibaldi Lake" srcset="../../img/2026/01/now/mountains/16.webp 2x" loading="lazy">
  <img alt="Garibaldi Lake" title="Garibaldi Lake" srcset="../../img/2026/01/now/mountains/17.webp 2x" loading="lazy">
  <img alt="Mount Weart view from Armchair Traverse" title="Mount Weart view from Armchair Traverse" srcset="../../img/2026/01/now/mountains/18.webp 2x" loading="lazy">
  <img alt="Lynn Ridge" title="Lynn Ridge" srcset="../../img/2026/01/now/mountains/19.webp 2x" loading="lazy">
  <img alt="Brunswick Mountain" title="Brunswick Mountain" srcset="../../img/2026/01/now/mountains/20.webp 2x" loading="lazy">
  <img alt="Atwell view from Slhanay" title="Atwell view from Slhanay" srcset="../../img/2026/01/now/mountains/21.webp 2x" loading="lazy">
</figure>
<h2 id="about-part-time-work" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#about-part-time-work"><span>About part-time work</span></a></h2>
<p>If you’re wondering how I go in the mountains that much, you need to
know I’ve been working part-time as a software contractor since 2021.
Going part-time was the best thing I’ve ever done in my career (other
than connecting a ping-pong table to the internet 🏓) and I’m incredibly
grateful to have worked with companies who gave me that opportunity.</p>
<p>Maybe I should write more about this because it’s still really uncommon
in the industry, but believe it or not, <strong>those years have also been the
most productive of my career</strong>.</p>
<p>It’s definitely a combination of factors. Despite working only 20 hours
a week:</p>
<ul>
<li>I’m naturally more experienced now than I was years ago.</li>
<li>I have been intentional about working with small companies (read: less
than 10 people) where I can have a sizable impact, without being
slowed down by corporate friction, processes and politics.</li>
<li><a href="https://en.wikipedia.org/wiki/Parkinson%27s_law">Parkinson’s law</a> is a thing (“work expands so as to fill the time available”).</li>
<li>And of course since a few years and increasingly, using AI.</li>
</ul>
<p>Still, this means that there’s a sweet spot for time-to-productivity
ratio, especially when it comes to creative work like building software,
and in many cases I’m not convinced that it’s at or above 40 hours.</p>
<figure class="grid grid-min-2 grid-fit">
  <img alt="Laptop with a view" title="Sea to Sky Gondola" srcset="../../img/2026/01/now/laptop/01.webp 2x" loading="lazy">
  <img alt="Laptop with a view" title="Sea to Sky Gondola" srcset="../../img/2026/01/now/laptop/02.webp 2x" loading="lazy">
</figure>
<h2 id="travel" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#travel"><span>Travel ✈️</span></a></h2>
<h3 id="mexico" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#mexico"><span>Mexico 🇲🇽</span></a></h3>
<p>Went to Mexico for a work retreat.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Tenochtitlan" title="Tenochtitlan" srcset="../../img/2026/01/now/mexico/01.webp 2x" loading="lazy">
  <img alt="Tenochtitlan" title="Tenochtitlan" srcset="../../img/2026/01/now/mexico/02.webp 2x" loading="lazy">
  <img alt="Turkish coffee (don’t ask)" title="Turkish coffee (don’t ask)" srcset="../../img/2026/01/now/mexico/03.webp 2x" loading="lazy">
  <img alt="Business meeting" title="Business meeting" srcset="../../img/2026/01/now/mexico/04.webp 2x" loading="lazy">
  <img alt="Mexico City" title="Mexico City" srcset="../../img/2026/01/now/mexico/05.webp 2x" loading="lazy">
  <img alt="Metropolitan Cathedral, view from an overpriced rooftop" title="Metropolitan Cathedral, view from an overpriced rooftop" srcset="../../img/2026/01/now/mexico/06.webp 2x" loading="lazy">
</figure>
<h3 id="montreal" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#montreal"><span>Montreal</span></a></h3>
<p>Did a pit stop by Montreal to catch up with friends and the 🔥 food
scene there.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="BBQ" srcset="../../img/2026/01/now/montreal/01.webp 2x" loading="lazy">
  <img alt="La Belle Tacos" title="La Belle Tacos" srcset="../../img/2026/01/now/montreal/02.webp 2x" loading="lazy">
  <img alt="Döner Istanbul" title="Döner Istanbul" srcset="../../img/2026/01/now/montreal/03.webp 2x" loading="lazy">
</figure>
<h3 id="toulouse" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#toulouse"><span>Toulouse</span></a></h3>
<p>Did a quick pit stop in Toulouse, which I visited for the first time,
since my flight landed there.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Toulouse street" srcset="../../img/2026/01/now/toulouse/01.webp 2x" loading="lazy">
  <img alt="Toulouse Ferris wheel" srcset="../../img/2026/01/now/toulouse/02.webp 2x" loading="lazy">
  <img alt="Place du Capitole" title="Place du Capitole" srcset="../../img/2026/01/now/toulouse/03.webp 2x" loading="lazy">
</figure>
<h3 id="andorra" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#andorra"><span>Andorra 🇦🇩</span></a></h3>
<p>Visited friends in Andorra, also first time for me there. There’s some
sick mountain biking and trail running. Lots of ridges to scramble,
everywhere, and all the mountains are connected by trails, so it feels
like there’s really no limits to the terrain!</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Coma Pedrosa" title="Coma Pedrosa" srcset="../../img/2026/01/now/andorra/01.webp 2x" loading="lazy">
  <img alt="Vall de Sorteny" title="Vall de Sorteny" srcset="../../img/2026/01/now/andorra/02.webp 2x" loading="lazy">
  <img alt="Arcalís" title="Arcalís" srcset="../../img/2026/01/now/andorra/03.webp 2x" loading="lazy">
  <img alt="Arcalís" title="Arcalís" srcset="../../img/2026/01/now/andorra/04.webp 2x" loading="lazy">
  <img alt="Arcalís" title="Arcalís" srcset="../../img/2026/01/now/andorra/05.webp 2x" loading="lazy">
  <img alt="Coma Pedrosa" title="Coma Pedrosa" srcset="../../img/2026/01/now/andorra/06.webp 2x" loading="lazy">
  <img alt="Font Blanca" title="Font Blanca" srcset="../../img/2026/01/now/andorra/07.webp 2x" loading="lazy">
  <img alt="Hippa!" title="Hippa!" srcset="../../img/2026/01/now/andorra/08.webp 2x" loading="lazy">
  <img alt="Arinsal bike park" title="Arinsal bike park" srcset="../../img/2026/01/now/andorra/09.webp 2x" loading="lazy">
</figure>
<h3 id="camargue" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#camargue"><span>Camargue 🦩</span></a></h3>
<p>Another pit stop at Saintes-Maries-de-la-Mer to cut a long drive in
half. Also first time here, very pretty!</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Beach" srcset="../../img/2026/01/now/camargue/01.webp 2x" loading="lazy">
  <img alt="Notre Dame de la Mer" title="Notre Dame de la Mer" srcset="../../img/2026/01/now/camargue/02.webp 2x" loading="lazy">
  <img alt="Flamingos!" title="Flamingos!" srcset="../../img/2026/01/now/camargue/03.webp 2x" loading="lazy">
</figure>
<h3 id="metz-and-nancy" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#metz-and-nancy"><span>Metz &amp; Nancy</span></a></h3>
<p>Finally some family time around Metz and Nancy.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Metz cathedral" title="Metz cathedral" srcset="../../img/2026/01/now/metz-1.webp 2x" loading="lazy">
  <img alt="Place Stanislas" title="Place Stanislas" srcset="../../img/2026/01/now/nancy-1.webp 2x" loading="lazy">
  <img alt="Nancy cathedral" title="Nancy cathedral" srcset="../../img/2026/01/now/nancy-2.webp 2x" loading="lazy">
</figure>
<h3 id="cakes" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#cakes"><span>Cakes 🍰</span></a></h3>
<p>I also happened to turn 30 on that trip. While I don’t believe the
specific number <em>means</em> much at all, I still see no reason to not treat
myself to 3 different cakes. 😜</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Cake" srcset="../../img/2026/01/now/cake/01.webp 2x" loading="lazy">
  <img alt="Cake" srcset="../../img/2026/01/now/cake/02.webp 2x" loading="lazy">
  <img alt="Cake" srcset="../../img/2026/01/now/cake/03.webp 2x" loading="lazy">
</figure>
<h3 id="nova-scotia" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#nova-scotia"><span>Nova Scotia 🦀</span></a></h3>
<p>I bundled all the above destinations together in July. Nova Scotia was a
separate trip.</p>
<p>A good friend of mine was getting married there, and I got to stand at
his wedding. Took that as an opportunity to go explore around the Cape
Breton and particularly ride the Cabot Trail on my friend’s road bike
that I borrowed. Around 300 km and 3,000 meters of elevation that I
split in two days. It was beautiful.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Cape Smokey" title="Cape Smokey" srcset="../../img/2026/01/now/nova-scotia/01.webp 2x" loading="lazy">
  <img alt="Pleasant Bay" title="Pleasant Bay" srcset="../../img/2026/01/now/nova-scotia/02.webp 2x" loading="lazy">
  <img alt="Chéticamp" title="Chéticamp" srcset="../../img/2026/01/now/nova-scotia/03.webp 2x" loading="lazy">
</figure>
<h2 id="shortest-move-ever" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#shortest-move-ever"><span>Shortest move ever</span></a></h2>
<p>At the end of the summer, my landlord sold the place I was renting, and
I had to move. I got lucky and found a place in the same building just
across the floor the same day I got the notice. Still can’t believe this
happened. 🙏</p>
<p>This made for a pretty easy and quite ridiculous move, with me dragging
my stuff across the hallway.</p>
<figure class="grid grid-min-2 grid-fit">
  <img alt="Moving" srcset="../../img/2026/01/now/move/01.webp 2x" loading="lazy">
  <img alt="Moving" srcset="../../img/2026/01/now/move/02.webp 2x" loading="lazy">
</figure>
<h2 id="the-big-jump" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#the-big-jump"><span>The big jump 🏃</span></a></h2>
<p>Remember how I was writing about part-time work earlier? Well all good
things have an end, and this was not an option anymore at the company I
had been working until then.</p>
<p>Going full-time right before ski season didn’t feel exactly right, and
it really felt like a sign for me to 1. take some proper time off and 2.
get out of my comfort zone and try something a bit scarier.</p>
<p>The time off consisted of a fuckton of climbing, then skiing. Being in
BC, we eventually got a few of the usual warm storms where it rains all
the way to the top of mountains and destroy the skiing conditions until
it snows again. <strong>This gave me some solid windows to start building
again.</strong></p>
<p>We got together with my friend <a href="https://www.benoitzohar.com/">Ben</a> and
started <a href="https://evetools.app/">EveTools</a>, where we make apps for
developers.</p>
<p>The first app is <a href="https://flame.evetools.app/">Flame</a>, and its goal is
to remove as much friction as possible from the Firebase local
development experience, especially when it comes to dealing with the
<a href="https://firebase.google.com/docs/emulator-suite">emulators</a>.</p>
<p>We’ve been working with Firebase quite extensively for over 3 years now,
so we have a pretty good understanding of the pain points of the current
experience. There’s a lot to do, and I hope we can have a positive
impact on that platform.</p>
<figure class="center">
  <img alt="Flame" srcset="../../img/2026/01/now/flame.webp 2x" loading="lazy">
</figure>
<p>At the time of writing it’s in private beta, but we’re planning on
opening it up pretty soon! If you find about Flame via this post,
<a href="https://www.codejam.info/val.html#contact">shoot me an email</a> and I’ll send you a discount! 🫶</p>
<hr>
<p>Two interesting realizations since this happened.</p>
<h3 id="buildingception" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#buildingception"><span>Buildingception</span></a></h3>
<p>First, building a product, and doing so end to end (read: not just the
tech), reveals <em>a lot</em> more pain points and problems to be solved. Some
specific to the stack we work with, and some broader ones too. And the
good news is, a lot of them can be solved with more tech!</p>
<p>In other words, the more I build, the more ideas I get about what to
build next.</p>
<p>Granted it’s not the best way to source ideas if you’re optimizing for
maximum revenue, but it seems to be doing a decent job at optimizing for
maximum motivation, and I do enjoy that for the time being.</p>
<h3 id="so-much-for-part-time" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#so-much-for-part-time"><span>So much for part-time</span></a></h3>
<p>Second, is that despite advocating for the benefits of part-time work
for the past 4 years, including higher up in this article, I’m actually
having a <em>really</em> good time working every single day right now, weekends
included. <strong>Working on your own thing does hit different.</strong></p>
<p>I hope my fellow coastal BC skiers don’t read this, but I’m lowkey
grateful for this rain-into-dry-spell event a few weeks ago, so that I
feel no guilt and <abbr title="Fear of missing out">FOMO</abbr>
to be so much on my laptop.</p>
<p>Ultimately, I still believe in part-time work, and in fact, being an
independent developer is my current best bet on building that lifestyle
back one day or another. But it also seems like a futile dream until I
generate any sustainable income.</p>
<p>Let’s see where the snow takes me…</p>
<h2 id="on-coffee" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#on-coffee"><span>On coffee ☕️</span></a></h2>
<p>I’m back to drinking caffeine, after years of being on decaf. 🤷‍♀️</p>
<p>Seems to pair well with the business hustling for now. We’ll see if I
build up a tolerance again. If it stops being a net positive I’ll
readjust.</p>
<p>And weirdly, after being into specialty coffee for years (read: small
batch micro roaster beans I weigh and grind before brewing), I’m back
with my moka pot and sous vide Italian pre-ground. Who would have
thought?</p>
<figure class="center">
  <img alt="Moka Pot with Italian coffee" srcset="../../img/2026/01/now/coffee.webp 2x" loading="lazy">
</figure>
<h2 id="a-few-words-on-2024" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#a-few-words-on-2024"><span>A few words on 2024</span></a></h2>
<p>I know, I know, I skipped the routine update last year. To be fair there
was no important update to write about, which is probably why I didn’t
feel the need to write. Still it included many cool adventures and trips
that I might as well slide in this post.</p>
<p>First there was the northern lights. Took the camera out for the
occasion.</p>
<div class="oversized">
  <figure class="grid grid-2">
    <img alt="Northern lights" srcset="../../img/2026/01/now/northern-lights/01.webp 2x" loading="lazy">
    <img alt="Northern lights" srcset="../../img/2026/01/now/northern-lights/02.webp 2x" loading="lazy">
  </figure>
</div>
<p>Of course, the usual local mountain adventures. Including skiing
Panorama Ridge, scrambling West Lion and Black Tusk, Mount Currie,
running a 60K to Diamond Head and Mamquam Lake, Sigurd Peak, biking the
Triple Crown (Cypress, Grouse and Seymour), and finishing with a
marathon to Overlord Mountain!</p>
<figure class="grid grid-min-2">
  <img style="--ratio: calc(720 / 2000)" alt="Black Tusk" title="Black Tusk" srcset="../../img/2026/01/now/mountains-2024/02.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Garibaldi Lake" title="Garibaldi Lake" srcset="../../img/2026/01/now/mountains-2024/01.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Watersprite Lake" title="Watersprite Lake" srcset="../../img/2026/01/now/mountains-2024/03.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Anderson Lake" title="Anderson Lake" srcset="../../img/2026/01/now/mountains-2024/04.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Summit of West Lion" title="Summit of West Lion" srcset="../../img/2026/01/now/mountains-2024/05.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Panorama Ridge and Garibaldi Lake view from Black Tusk" title="Panorama Ridge and Garibaldi Lake view from Black Tusk" srcset="../../img/2026/01/now/mountains-2024/06.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Black Tusk view from Panorama Ridge" title="Black Tusk view from Panorama Ridge" srcset="../../img/2026/01/now/mountains-2024/07.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Garibaldi Lake" title="Garibaldi Lake" srcset="../../img/2026/01/now/mountains-2024/08.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Garibaldi cluster and glacier" title="Garibaldi cluster and glacier" srcset="../../img/2026/01/now/mountains-2024/09.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Castle Towers view from Musical Bumps" title="Castle Towers view from Musical Bumps" srcset="../../img/2026/01/now/mountains-2024/10.webp 2x" loading="lazy">
</figure>
<p>Went to Jackson Hole for a work retreat, and took some extra time off
there to explore the mountains and do a lot of road and gravel biking,
including riding around Yellowstone, with a colleague who’s an absolute
triathlon machine. Those few weeks are probably the most volume I’ve
done in a continuous stretch to this day, although the Andorra trip from
last year was a close contender.</p>
<figure class="grid grid-min-2">
  <img style="--ratio: calc(1280 / 2000)" alt="The Tetons view from Jackson Lake" title="The Tetons view from Jackson Lake" srcset="../../img/2026/01/now/wyoming/01.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="The Tetons view from Cascade Canyon" title="The Tetons view from Cascade Canyon" srcset="../../img/2026/01/now/wyoming/02.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="4Runner in front of the Tetons" title="4Runner in front of the Tetons" srcset="../../img/2026/01/now/wyoming/04.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="The Tetons view from Cascade Canyon" title="The Tetons view from Cascade Canyon" srcset="../../img/2026/01/now/wyoming/03.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Yellowstone" title="Yellowstone" srcset="../../img/2026/01/now/wyoming/05.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Yellowstone" title="Yellowstone" srcset="../../img/2026/01/now/wyoming/06.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Grand Teton view from the saddle" title="Grand Teton view from the saddle" srcset="../../img/2026/01/now/wyoming/08.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="Middle Teton view from the Grand Teton approach" title="Middle Teton view from the Grand Teton approach" srcset="../../img/2026/01/now/wyoming/07.webp 2x" loading="lazy">
  <img style="--ratio: calc(1280 / 2000)" alt="The Tetons view from Cascade Canyon" title="The Tetons view from Cascade Canyon" srcset="../../img/2026/01/now/wyoming/09.webp 2x" loading="lazy">
  <img style="--ratio: calc(720 / 2000)" alt="Icefloe Lake" title="Icefloe Lake" srcset="../../img/2026/01/now/wyoming/10.webp 2x" loading="lazy">
</figure>
<p>Also went sailing in Majorca with a few friends!</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Puig de Massanella" title="Puig de Massanella" srcset="../../img/2026/01/now/majorca/01.webp 2x" loading="lazy">
  <img alt="Sant Elm" title="Sant Elm" srcset="../../img/2026/01/now/majorca/02.webp 2x" loading="lazy">
  <img alt="Sant Elm" title="Sant Elm" srcset="../../img/2026/01/now/majorca/03.webp 2x" loading="lazy">
  <img alt="San Miguel 0.0% on a sailboat" srcset="../../img/2026/01/now/majorca/04.webp 2x" loading="lazy">
  <img alt="Cabrera" title="Cabrera" srcset="../../img/2026/01/now/majorca/05.webp 2x" loading="lazy">
  <img alt="Cabrera" title="Cabrera" srcset="../../img/2026/01/now/majorca/06.webp 2x" loading="lazy">
  <img alt="Cabrera" title="Cabrera" srcset="../../img/2026/01/now/majorca/07.webp 2x" loading="lazy">
  <img alt="Cabrera" title="Cabrera" srcset="../../img/2026/01/now/majorca/08.webp 2x" loading="lazy">
  <img alt="Cala Figuera" title="Cala Figuera" srcset="../../img/2026/01/now/majorca/09.webp 2x" loading="lazy">
  <img alt="Cala Figuera" title="Cala Figuera" srcset="../../img/2026/01/now/majorca/10.webp 2x" loading="lazy">
  <img alt="Sunset on a sailboat" srcset="../../img/2026/01/now/majorca/11.webp 2x" loading="lazy">
  <img alt="Montgrí Castle" title="Montgrí Castle" srcset="../../img/2026/01/now/majorca/12.webp 2x" loading="lazy">
</figure>
<p>And finished by some trail running in the Vosges.</p>
<figure class="grid grid-3 grid-min-2">
  <img alt="Moss on a log" srcset="../../img/2026/01/now/vosges/01.webp 2x" loading="lazy">
  <img alt="Hohneck" title="Hohneck" srcset="../../img/2026/01/now/vosges/02.webp 2x" loading="lazy">
  <img alt="Schiessrothried Lake" title="Schiessrothried Lake" srcset="../../img/2026/01/now/vosges/03.webp 2x" loading="lazy">
  <img alt="Col du Schaeferthal" title="Col du Schaeferthal" srcset="../../img/2026/01/now/vosges/04.webp 2x" loading="lazy">
  <img alt="Croziflette" title="Croziflette" srcset="../../img/2026/01/now/vosges/05.webp 2x" loading="lazy">
  <img alt="Grand Ballon" title="Grand Ballon" srcset="../../img/2026/01/now/vosges/06.webp 2x" loading="lazy">
</figure>
<h2 id="on-reflecting" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2026/01/the-next-chapter.html#on-reflecting"><span>On reflecting</span></a></h2>
<p>Putting together all those photos is pretty time consuming, but honestly
it feels valuable to take that time to reflect on the past and really
appreciate everything that I experienced.</p>
<p>It’s easy to get caught up in the fast paced train of life and never
take a step back to contemplate the big picture. Those “now page”
updates help me do exactly that.</p>
]]></content>
  </entry>
  <entry>
    <title>macOS app showing scrollbars to some users only</title>
    <link href="https://www.codejam.info/2025/06/macos-app-scrollbars-some-users-only.html" />
    <id>https://www.codejam.info/2025/06/macos-app-scrollbars-some-users-only.html</id>
    <updated>2025-06-25T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<h2 id="tldr" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/macos-app-scrollbars-some-users-only.html#tldr"><span>TLDR</span></a></h2>
<p>macOS has a setting where it shows fixed scrollbars to people using a
mouse, while people using a trackpad have tiny “floating” scrollbars
that are entirely hidden when not actively scrolling.</p>
<p>You can find it in <strong>Appearance &gt; Show scroll bars</strong> and either force it
to always on or always off (or keep it auto).</p>
<h2 id="story-time" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/macos-app-scrollbars-some-users-only.html#story-time"><span>Story time</span></a></h2>
<p>While developing a macOS app (in my case with Electron, although this
problem is not specific to Electron), I encountered a wild bug.</p>
<p>Some users reported having scrollbars visible in different parts of the
app, and looking quite “off” visually:</p>
<figure class="center">
  <img alt="Scrollbars" srcset="../../img/2025/06/macos-scrollbars/scrollbars.png 2x">
</figure>
<p>However on my side it looked just fine:</p>
<figure class="center">
  <img alt="No scrollbars" srcset="../../img/2025/06/macos-scrollbars/no-scrollbars.png 2x">
</figure>
<p>I could not understand what was causing this. After all, it was
happening across the same version of Electron, Chromium, of the app
itself, and of macOS!</p>
<p>It’s only when another developer reported having the bug that I jumped
on a call with him to debug the issue.</p>
<p>While messing with the dev tools, one thing jumped out to me: inside his
dev tools, scrollbars were visible at all times:</p>
<figure class="center">
  <img alt="Dev tools scrollbars" srcset="../../img/2025/06/macos-scrollbars/devtools-scrollbars.png 2x">
</figure>
<p>While on my side I only had floating scrollbars while I was scrolling,
and they would disappear entirely otherwise:</p>
<figure class="center">
  <img alt="Dev tools floating scrollbars" srcset="../../img/2025/06/macos-scrollbars/devtools-floating.png 2x">
</figure>
<p>Same version of macOS as well!</p>
<p>What was going on?</p>
<p><strong>It turns out the culprit was that my colleague was using a mouse,
while I was using the trackpad of my laptop.</strong></p>
<p>What the mouse vs. trackpad have to do with this? Meet this setting:</p>
<figure class="center">
  <img alt="macOS scrollbars settings" srcset="../../img/2025/06/macos-scrollbars/macos-settings.png 2x">
</figure>
<p>By default, macOS shows scrollbars at all times when using a mouse, but
uses floating scrollbars when using a trackpad!</p>
<p>Now I could force this setting to “Always” to reproduce the issue
locally, and update the app to look OK even for people who use a mouse. 🙃</p>
<p>In many places, we had unnecessary <code>overflow: scroll</code> in places where
<code>overflow: auto</code> would do, and a few places where we had to hide
scrollbars entirely with <code>overflow: hidden</code>.</p>
<p>It was easy to make those silly mistakes when all the devs working on
the app were using a trackpad. 😂</p>
<h2 id="bonus-dark-mode-scrollbars" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/macos-app-scrollbars-some-users-only.html#bonus-dark-mode-scrollbars"><span>Bonus: dark mode scrollbars</span></a></h2>
<p>For the places where we did want scrollbars, there was a remaining
issue: we force our app in dark mode, but the scrollbars were showing in
light mode nevertheless!</p>
<p>The way we force our Electron app in dark mode is by doing:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> { nativeTheme } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;electron&#x27;</span>

nativeTheme.<span class="hljs-property">themeSource</span> = <span class="hljs-string">&#x27;dark&#x27;</span>
</code></pre>
<p>However in order for native scrollbars to show in dark mode, we also had
to add the following to our HTML file:</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">body</span> <span class="hljs-attr">style</span>=<span class="hljs-string">&quot;color-scheme: dark&quot;</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
</code></pre>
<p>(This is not the place where we actually needed scrollbars, but for lack
of a better screenshot, here’s what it would look like with both
directions scrollbars:)</p>
<figure class="center">
  <img alt="Scrollbars dark mode" srcset="../../img/2025/06/macos-scrollbars/scrollbars-dark.png 2x">
</figure>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Join the discussion on <a href="https://x.com/valeriangalliat/status/1938046832753750198">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>GitHub Action hanging on Electron macOS app code signing</title>
    <link href="https://www.codejam.info/2025/06/github-action-hanging-macos-app-code-signing.html" />
    <id>https://www.codejam.info/2025/06/github-action-hanging-macos-app-code-signing.html</id>
    <updated>2025-06-22T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>There’s quite a bunch of resources about how to set up macOS app code
signing on GitHub Actions:</p>
<ul>
<li><a href="https://dev.to/rwwagner90/signing-electron-apps-with-github-actions-4cof">Signing Electron apps with GitHub Actions</a></li>
<li><a href="https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development">Installing an Apple certificate on macOS runners for Xcode development</a></li>
<li><a href="https://brunoscheufler.com/blog/2023-11-12-setting-up-hosted-macos-github-actions-workflows-for-electron-builds">Setting up hosted macOS GitHub Actions workflows for Electron
builds</a></li>
</ul>
<p>But what to do if your GitHub workflow just hangs forever at the
Electron signing step?</p>
<p>This appears to be a symptom of not properly configuring the macOS
Keychain in the CI environment. Sadly there could be a bunch of reasons
for that and the hanging doesn’t really tell us which one specifically
is a problem.</p>
<p>There’s <a href="https://github.com/electron/forge/issues/3315">two</a>
<a href="https://github.com/electron/packager/issues/701">issues</a> I found with
that hanging problem, and the solutions there may or may not solve the
problem for you.</p>
<p>What I’m gonna suggest though is really double checking the code you’re
using to import the signing keys based on all the links I shared above,
and see if you’re missing something.</p>
<p>If it can help, here’s the code that ended up working for me:</p>
<pre><code class="hljs language-yaml"><span class="hljs-attr">name:</span> <span class="hljs-string">Release</span>

<span class="hljs-attr">on:</span>
  <span class="hljs-attr">push:</span>
    <span class="hljs-attr">branches:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">main</span>

<span class="hljs-attr">jobs:</span>
  <span class="hljs-attr">release:</span>
    <span class="hljs-attr">runs-on:</span> <span class="hljs-string">macos-latest</span>

    <span class="hljs-attr">steps:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/checkout@v4</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">actions/setup-node@v4</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">uses:</span> <span class="hljs-string">swift-actions/setup-swift@v2</span>

      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">Import</span> <span class="hljs-string">certificate</span>
        <span class="hljs-attr">env:</span>
          <span class="hljs-attr">PRIVATE_KEY:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.PRIVATE_KEY</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">PRIVATE_KEY_PASSWORD:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.PRIVATE_KEY_PASSWORD</span> <span class="hljs-string">}}</span>
          <span class="hljs-attr">CERTIFICATE:</span> <span class="hljs-string">${{</span> <span class="hljs-string">secrets.CERTIFICATE</span> <span class="hljs-string">}}</span>
        <span class="hljs-attr">run:</span> <span class="hljs-string">|
          # Create a temporary keychain
          security create-keychain -p &quot;&quot; build.keychain
</span>
          <span class="hljs-comment"># Set it as default for the user session</span>
          <span class="hljs-string">security</span> <span class="hljs-string">default-keychain</span> <span class="hljs-string">-s</span> <span class="hljs-string">build.keychain</span>

          <span class="hljs-string">security</span> <span class="hljs-string">unlock-keychain</span> <span class="hljs-string">-p</span> <span class="hljs-string">&quot;&quot;</span> <span class="hljs-string">build.keychain</span>

          <span class="hljs-comment"># Set it to lock in 1 hour (should be long enough, and probs longer than the macOS default that could be too short)</span>
          <span class="hljs-string">security</span> <span class="hljs-string">set-keychain-settings</span> <span class="hljs-string">-t</span> <span class="hljs-number">3600</span> <span class="hljs-string">-l</span> <span class="hljs-string">~/Library/Keychains/build.keychain</span>

          <span class="hljs-string">mkdir</span> <span class="hljs-string">-p</span> <span class="hljs-string">~/certificates</span>
          <span class="hljs-string">cd</span> <span class="hljs-string">~/certificates</span>

          <span class="hljs-string">echo</span> <span class="hljs-string">&quot;$PRIVATE_KEY&quot;</span> <span class="hljs-string">|</span> <span class="hljs-string">base64</span> <span class="hljs-string">--decode</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">&quot;My App.p12&quot;</span>
          <span class="hljs-string">echo</span> <span class="hljs-string">&quot;CERTIFICATE&quot;</span> <span class="hljs-string">|</span> <span class="hljs-string">base64</span> <span class="hljs-string">--decode</span> <span class="hljs-string">&gt;</span> <span class="hljs-string">&quot;My App.cer&quot;</span>

          <span class="hljs-string">security</span> <span class="hljs-string">import</span> <span class="hljs-string">&quot;My App.p12&quot;</span> <span class="hljs-string">-k</span> <span class="hljs-string">build.keychain</span> <span class="hljs-string">-P</span> <span class="hljs-string">&quot;$PRIVATE_KEY_PASSWORD&quot;</span> <span class="hljs-string">-T</span> <span class="hljs-string">/usr/bin/codesign</span>
          <span class="hljs-string">security</span> <span class="hljs-string">import</span> <span class="hljs-string">&quot;My App.cer&quot;</span> <span class="hljs-string">-k</span> <span class="hljs-string">build.keychain</span> <span class="hljs-string">-T</span> <span class="hljs-string">/usr/bin/codesign</span>

          <span class="hljs-comment"># Check certificates</span>
          <span class="hljs-string">security</span> <span class="hljs-string">find-identity</span> <span class="hljs-string">-v</span> <span class="hljs-string">-p</span> <span class="hljs-string">codesigning</span> <span class="hljs-string">build.keychain</span>

          <span class="hljs-comment"># Add keychain to search list</span>
          <span class="hljs-string">security</span> <span class="hljs-string">list-keychains</span> <span class="hljs-string">-d</span> <span class="hljs-string">user</span> <span class="hljs-string">-s</span> <span class="hljs-string">build.keychain</span>
          <span class="hljs-string">security</span> <span class="hljs-string">set-key-partition-list</span> <span class="hljs-string">-S</span> <span class="hljs-string">apple-tool:,apple:,codesign:</span> <span class="hljs-string">-s</span> <span class="hljs-string">-k</span> <span class="hljs-string">&quot;&quot;</span> <span class="hljs-string">build.keychain</span>

      <span class="hljs-comment"># ...</span>
</code></pre>
<p>Here, <code>PRIVATE_KEY</code> is a Base64-encoded version of the <code>.p12</code> private
key, password protected by <code>PRIVATE_KEY_PASSWORD</code>.</p>
<p><code>CERTIFICATE</code> is a Base64-encoded version of the <code>.cer</code> file that Apple
provided for your application.</p>
<p>Hope that helps!</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>USB iPhone screen recording in Swift</title>
    <link href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html" />
    <id>https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html</id>
    <updated>2025-06-22T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>When you plug in an iPhone to a Mac via USB, QuickTime allows you to
select the iPhone screen as a video recording source.</p>
<p>This is neat, but what if you want to do do the same thing from your own
app?</p>
<p>I’ve had to do this recently, so this blog post will compile everything
I learnt about and especially the undocumented quirks I encountered and
worked around.</p>
<h2 id="kcmiohardwarepropertyallowscreencapturedevices" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#kcmiohardwarepropertyallowscreencapturedevices"><span><code>kCMIOHardwarePropertyAllowScreenCaptureDevices</code></span></a></h2>
<p>The very first thing you need is to enable
<a href="https://developer.apple.com/documentation/coremediaio/kcmiohardwarepropertyallowscreencapturedevices"><code>kCMIOHardwarePropertyAllowScreenCaptureDevices</code></a>.</p>
<p>This is a “hardware property” (whatever that means) that, when set,
allows the current process to access USB-connected mobile devices for
screen recording.</p>
<p>You can find
<a href="https://gist.github.com/samjoch/d06f7fb39b2cbbca087ddcb1af59b28e">many</a>
<a href="https://nadavrub.wordpress.com/2015/07/06/macos-media-capture-using-coremediaio/">flavors</a>
of how to do this online, and here’s mine anyway:</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">import</span> CoreMediaIO

<span class="hljs-comment">// Sets the &quot;hardware&quot; prop that allows to discover USB mobile devices for screen recording.</span>
<span class="hljs-keyword">func</span> <span class="hljs-title function_">allowScreenCaptureDevices</span>() {
  <span class="hljs-keyword">let</span> element: <span class="hljs-type">CMIOObjectPropertyElement</span>
  <span class="hljs-keyword">if</span> <span class="hljs-keyword">#available</span>(<span class="hljs-keyword">macOS</span> <span class="hljs-number">12.0</span>, <span class="hljs-operator">*</span>) {
    element <span class="hljs-operator">=</span> <span class="hljs-type">CMIOObjectPropertyElement</span>(kCMIOObjectPropertyElementMain)
  } <span class="hljs-keyword">else</span> {
    element <span class="hljs-operator">=</span> <span class="hljs-type">CMIOObjectPropertyElement</span>(kCMIOObjectPropertyElementMaster)
  }

  <span class="hljs-keyword">var</span> prop <span class="hljs-operator">=</span> <span class="hljs-type">CMIOObjectPropertyAddress</span>(
    mSelector: <span class="hljs-type">CMIOObjectPropertySelector</span>(kCMIOHardwarePropertyAllowScreenCaptureDevices),
    mScope: <span class="hljs-type">CMIOObjectPropertyScope</span>(kCMIOObjectPropertyScopeGlobal),
    mElement: element)

  <span class="hljs-keyword">var</span> allow: <span class="hljs-type">UInt32</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>
  <span class="hljs-keyword">let</span> dataSize: <span class="hljs-type">UInt32</span> <span class="hljs-operator">=</span> <span class="hljs-number">4</span>
  <span class="hljs-keyword">let</span> zero: <span class="hljs-type">UInt32</span> <span class="hljs-operator">=</span> <span class="hljs-number">0</span>

  <span class="hljs-type">CMIOObjectSetPropertyData</span>(
    <span class="hljs-type">CMIOObjectID</span>(kCMIOObjectSystemObject), <span class="hljs-operator">&amp;</span>prop, zero, <span class="hljs-literal">nil</span>, dataSize, <span class="hljs-operator">&amp;</span>allow)
}
</code></pre>
<p>This will allow you to discover USB mobile devices as part of your usual
<code>AVCaptureDevice.DiscoverySession</code>.</p>
<p>Now while this code uses a relatively verbose low-level old C interface
(because it’s the only way to do this right now), it’s fairly
straightforward. It’s spiritually equivalent to doing
<code>kCMIOHardwarePropertyAllowScreenCaptureDevices = 1</code> (no shit).</p>
<p>But this hardware property is not as innocent as it looks, and I’m about
to infodump on you everything I found out about it. Brace yourselves (or
skip to the next section until you encounter weird issues and need to
come back here 😂).</p>
<h2 id="it-s-not-instant" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#it-s-not-instant"><span>It’s not instant</span></a></h2>
<p>When you set <code>kCMIOHardwarePropertyAllowScreenCaptureDevices</code>, the
effect is not instant, meaning if you do a
<code>AVCaptureDevice.DiscoverySession</code> right after, you’re basically
guaranteed to <em>not</em> see the connected USB mobile devices.</p>
<p>This is not necessarily a problem. For example if your Swift process is
long-running, you set that prop first thing on boot, but you actually
list the devices later on upon user interaction, everything will be
fine.</p>
<p>However if you’re working with a CLI (i.e. <code>my-cli list-devices</code>
/ <code>my-cli record-device &lt;device&gt;</code>), or simply need access to the
mobile devices immediately upon starting the app, this is not gonna cut
it.</p>
<p>After setting the prop, the devices are gonna take up to a few seconds
to “show up”, and you can listen to the <code>AVCaptureDeviceWasConnected</code>
notification from the <code>NotificationCenter</code> to know about it. There’s a
good example for that in <a href="https://gist.github.com/samjoch/d06f7fb39b2cbbca087ddcb1af59b28e">this Gist</a>.</p>
<pre><code class="hljs language-swift"><span class="hljs-comment">// See &quot;The get devices warmup side-effect&quot; below for why this is necessary...</span>
<span class="hljs-keyword">let</span> <span class="hljs-keyword">_</span> <span class="hljs-operator">=</span> <span class="hljs-type">AVCaptureDevice</span>.devices()

<span class="hljs-type">NotificationCenter</span>.default
  .addObserver(
    forName: <span class="hljs-type">NSNotification</span>.<span class="hljs-type">Name</span>.<span class="hljs-type">AVCaptureDeviceWasConnected</span>, object: <span class="hljs-literal">nil</span>, queue: <span class="hljs-literal">nil</span>
  ) { (notif) -&gt; <span class="hljs-type">Void</span> <span class="hljs-keyword">in</span>
    <span class="hljs-keyword">let</span> device <span class="hljs-operator">=</span> notif.object<span class="hljs-operator">!</span> <span class="hljs-keyword">as!</span> <span class="hljs-type">AVCaptureDevice</span>
    <span class="hljs-comment">// ...</span>
  }
</code></pre>
<p>This works, but it also means there’s no way to tell immediately that
<em>no</em> device is currently connected. This is a problem if you want
to implement <code>my-cli list-devices</code>. Your best option is to time out
after a few seconds, but it’s not ideal because of the added delay when
no device is connected…</p>
<h2 id="the-get-devices-warmup-side-effect" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#the-get-devices-warmup-side-effect"><span>The get devices warmup side-effect</span></a></h2>
<p>This one is super sneaky and I wasted a lot of time on it. It turns out
that if you don’t call an API to list the devices, i.e. the deprecated
<code>AVCaptureDevice.devices</code>, or now a proper
<code>AVCaptureDevice.DiscoverySession</code>, the <code>AVCaptureDeviceWasConnected</code>
notification will never arrive.</p>
<p>So you need to start a <code>DiscoverySession</code> first, expecting to get 0
devices back (because you just set the hardware prop and its effect is
not instant), just to “warm up” the system, so that it will actually
send the notification.</p>
<p>In the <a href="https://gist.github.com/samjoch/d06f7fb39b2cbbca087ddcb1af59b28e#file-avcapturedevice-playground-swift-L38">Gist</a>
I linked earlier, the <code>print(&quot;\(AVCaptureDevice.devices().count)&quot;)</code> line
is actually <em>significant</em> and the code will <em>not</em> work without it:</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">func</span> <span class="hljs-title function_">start</span>() {
  <span class="hljs-built_in">print</span>(<span class="hljs-string">&quot;<span class="hljs-subst">\(AVCaptureDevice.devices().count)</span>&quot;</span>)

  <span class="hljs-type">NotificationCenter</span>.default
    .addObserver(
      forName: <span class="hljs-type">NSNotification</span>.<span class="hljs-type">Name</span>.<span class="hljs-type">AVCaptureDeviceWasConnected</span>, object: <span class="hljs-literal">nil</span>, queue: <span class="hljs-literal">nil</span>
    ) { (notif) -&gt; <span class="hljs-type">Void</span> <span class="hljs-keyword">in</span>
      <span class="hljs-keyword">self</span>.iosDeviceAttached(device: notif.object<span class="hljs-operator">!</span> <span class="hljs-keyword">as!</span> <span class="hljs-type">AVCaptureDevice</span>)
    }
}
</code></pre>
<p>It’s not just the innocent debug print that it seems. The fact it calls
<code>AVCaptureDevice.devices</code> is what allows to warm up the system and for
the notification to actually be sent later on. Without it, the
notification will <em>never</em> arrive.</p>
<p>I like to make it a bit more explicit with:</p>
<pre><code class="hljs language-swift"><span class="hljs-comment">// We don&#x27;t need the data but this appears to be required to &quot;warm up&quot;</span>
<span class="hljs-comment">// the system. If we don&#x27;t make the system call to get devices first,</span>
<span class="hljs-comment">// we can&#x27;t discover new devices with `AVCaptureDeviceWasConnected`. 🤷</span>
<span class="hljs-keyword">let</span> <span class="hljs-keyword">_</span> <span class="hljs-operator">=</span> <span class="hljs-type">AVCaptureDevice</span>.devices()
</code></pre>
<h2 id="it-s-rate-limited" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#it-s-rate-limited"><span>It’s rate limited?</span></a></h2>
<p>This one also made me pull my hair out for a while. So I start my app
that sets the above hardware prop, listen to device connected
notifications, and can see the iPhone available for screen recording.</p>
<p>Then I iterate on my code, maybe add some logging or write come code to
actually start capturing the video feed, and then restart the app.</p>
<p>And then, not only setting
<code>kCMIOHardwarePropertyAllowScreenCaptureDevices</code> takes <em>a few long
blocking seconds</em> to complete, but on top of that I don’t ever get any
device connected notification despite the iPhone being plugged in!</p>
<p>It appears to me that setting this prop is somehow rate limited. I would
need to wait around a minute before launching my CLI again in order for
it to behave “normally” (where setting the prop is near-instant, and I
do get a notification for the plugged-in devices).</p>
<p>However, and that’s where it gets interesting, I noticed that if any
other process on the computer also sets that same hardware property
(i.e. QuickTime), and that process stays running in the background, then
my CLI would reliably work every single time, even if I launch it many
times in a short time span. So it’s like that “rate limit” is really an
issue if my CLI is the <em>only</em> process on the system to set that prop.</p>
<p>So what did I do? I made a <code>my-cli background</code> command that literally
only sets the <code>kCMIOHardwarePropertyAllowScreenCaptureDevices</code> prop,
then sleeps indefinitely. Ran that in the background, then could do
<code>my-cli list-devices</code> and so on as much as I wanted.</p>
<p>Wasn’t gonna cut it for me for production, but at least that was useful
during development to allow me to iterate quickly.</p>
<h2 id="actual-device-recording" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#actual-device-recording"><span>Actual device recording</span></a></h2>
<p>I won’t go in much details here because there’s actually no quirks on
that side of things. It’s just your typical <code>AVFoundation</code> recording
which is very well covered online already.</p>
<p>First we get the external devices via a <code>DiscoverySession</code>:</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">let</span> devices <span class="hljs-operator">=</span> <span class="hljs-type">AVCaptureDevice</span>.<span class="hljs-type">DiscoverySession</span>(
  deviceTypes: [.external],
  <span class="hljs-comment">// Muxed type seems to be a decent way to distinguish USB connected</span>
  <span class="hljs-comment">// mobile devices from other external devices like e.g. &quot;OBS Virtual Camera&quot;.</span>
  mediaType: .muxed,
  position: .unspecified
).devices
</code></pre>
<p>This returns a list of <code>AVCaptureDevice</code>. Alternatively if we have the
ID of a mobile device already:</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">let</span> device <span class="hljs-operator">=</span> <span class="hljs-type">AVCaptureDevice</span>(uniqueID: <span class="hljs-string">&quot;...&quot;</span>)
</code></pre>
<p>Then we make an input from that device:</p>
<pre><code class="hljs language-swift"><span class="hljs-keyword">let</span> deviceInput <span class="hljs-operator">=</span> <span class="hljs-keyword">try</span> <span class="hljs-type">AVCaptureDeviceInput</span>(device: device)
</code></pre>
<p>And the rest is the usual <a href="https://developer.apple.com/documentation/avfoundation/setting-up-a-capture-session"><code>AVCaptureSession</code> protocol</a>.</p>
<h2 id="wrapping-up" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/06/usb-iphone-screen-recording-swift.html#wrapping-up"><span>Wrapping up</span></a></h2>
<p>If you encountered any if the quirks above, I hope that it helped you
work around them and hopefully you didn’t waste as much time on this as
I did. Happy device recording!</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Turborepo: don’t buffer logs on GitHub Actions</title>
    <link href="https://www.codejam.info/2025/04/turborepo-buffer-logs-github-actions.html" />
    <id>https://www.codejam.info/2025/04/turborepo-buffer-logs-github-actions.html</id>
    <updated>2025-04-23T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>When using <code>turbo run</code> inside a GitHub workflow, Turborepo buffers the
logs by package so it’s all neatly sorted in the output.</p>
<p>This is a nice feature when you’re looking at the logs after a run is
completed.</p>
<p>However when you’re trying to debug a timing-based issue <em>live</em>, this
makes it really annoying because the logs are buffered the entire time
the process runs and only spit out at the end! And then we can’t see
which package logged what first because the logs got grouped by package
instead of being in a merged stream.</p>
<p>I ended up dissecting the source of Turborepo to figure out what causes
this behavior inside GitHub Actions. <a href="https://github.com/vercel/turborepo/blob/90369bd86cd11ae59d5f94f60bcdbe49313d065f/crates/turborepo-ci/src/vendors.rs#L262-L285">I found it</a>.</p>
<p>It’s triggered by the <code>GITHUB_ACTIONS</code> environment variable being
present.</p>
<p>So running <code>unset GITHUB_ACTIONS</code> before running <code>turbo run</code> turns off
this behavior!</p>
<p>In theory that should suffice but I had the following in my notes so
I’ll also leave it here in case it helps:</p>
<pre><code class="hljs language-sh"><span class="hljs-built_in">unset</span> $(<span class="hljs-built_in">env</span> | grep RUNNER | <span class="hljs-built_in">cut</span> -d= -f1)
<span class="hljs-built_in">unset</span> $(<span class="hljs-built_in">env</span> | grep GITHUB | <span class="hljs-built_in">cut</span> -d= -f1)
</code></pre>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Firestore Algolia full index operation error 400 or 403</title>
    <link href="https://www.codejam.info/2025/04/firestore-algolia-full-index-error.html" />
    <id>https://www.codejam.info/2025/04/firestore-algolia-full-index-error.html</id>
    <updated>2025-04-23T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>I was testing a change to the Algolia setup in a staging environment and
needed to perform a full index.</p>
<h2 id="triggering-a-full-index" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/04/firestore-algolia-full-index-error.html#triggering-a-full-index"><span>Triggering a full index</span></a></h2>
<p>BTW to trigger a full index, either change the Algolia extension config
from the Firebase console, and make sure that the <strong>Full Index existing
documents</strong> field is set to true.</p>
<p>If it’s already on true (so that won’t trigger an actual config change)
and you don’t want to change anything else in the config, you can set it
to false, wait 5 minutes for it to deploy, then set it to true again
(lol). Or, better, go in Google Cloud Console, in Cloud Task, and for
the <code>ext-firestore-algolia-search-executeFullIndexOperation</code> task queue,
select <strong>Actions &gt; Force a task run</strong>.</p>
<h2 id="errors-during-full-index" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2025/04/firestore-algolia-full-index-error.html#errors-during-full-index"><span>Errors during full index</span></a></h2>
<p>I was looking at the
<code>ext-firestore-algolia-search-executeFullIndexOperation</code> Cloud Function
logs to monitor the index operation, and after a few minutes it choked
with a 400 error (but I’ve also seen 403).</p>
<p>The logs are really unhelpful because the request body gets logged
entirely first, but in my case it’s too big truncates the log line, and
so if this function logs the response body (which I’m not even sure),
it’s not accessible because the line got truncated in the request part.</p>
<p>It took me longer than I’m willing to admit to figure that, but I found
out that there’s a <strong>API Monitoring</strong> section on Algolia where we can
<strong>Search API Logs</strong> and I could see the 400 and 403 errors there!</p>
<p>In my case the errors were:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Record at the position ... objectID=... is too big size=.../10000 bytes. Please have a look at https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/in-depth/index-and-records-size-and-usage-limitations/#record-size-limits&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">400</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;You have exceeded your Record quota. You’ll need to change your plan for more capacity, or delete records. See more details at https://www.algolia.com/account/billing/details?applicationId=...&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">403</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>That’s much more useful.</p>
<p>Turns out my staging env had too many records already for its plan (free
plan) so I couldn’t sync more during the full index, and in another case
the record I was trying to write was too big for the free plan max
record size.</p>
<p>I wasn’t having the error in prod because we use a paid plan there.</p>
<p>So the solution was simple. Upgrade staging to a paid plan so I can do
my testing!</p>
<p>After that I triggered a full index again and it worked just fine. 🙏</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>How to sign and notarize an Electron app for macOS</title>
    <link href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html" />
    <id>https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html</id>
    <updated>2024-05-15T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>So you made a macOS app, shared it with your friends, and they
encountered one of those dreaded popups:</p>
<figure class="grid">
  <img alt="App cannot be opened because it is from an unidentified developer" srcset="../../img/2024/05/electron-signature/unidentified-developer.png 2x">
  <img alt="App can’t be opened because Apple cannot check it for malicious software" srcset="../../img/2024/05/electron-signature/cannot-check.png 2x">
</figure>
<p>Then you need to sign (left) and notarize (right) your app!</p>
<div class="note">
<p><strong>Note:</strong> in the meantime, the app
<a href="https://support.apple.com/en-ca/guide/mac-help/mh40616/mac">can still</a>
<a href="https://support.apple.com/en-ca/guide/mac-help/mchleab3a043/mac">be opened</a>
by right clicking on it and clicking <strong>Open</strong> from the context menu.</p>
</div>
<p>Signing consists in buying a developer membership with Apple, which will
let you create a key that you can use to <code>codesign</code> your app with.</p>
<p>Notarizing consists in uploading your app to an Apple service that scans
it for malware. If it passes the process, your app gets a “stamp of
approval” that is <a href="https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution">bundled with your app and also mirrored</a>
on Apple’s Gatekeeper servers.</p>
<p>In the case of Electron, here’s some relevant docs this article is based
on:</p>
<ul>
<li><a href="https://www.electronjs.org/docs/latest/tutorial/code-signing#signing--notarizing-macos-builds">Electron: Signing &amp; notarizing macOS builds</a></li>
<li><a href="https://www.electronforge.io/guides/code-signing/code-signing-macos">Electron Forge: Signing a macOS app</a></li>
</ul>
<h2 id="get-a-signing-certificate" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#get-a-signing-certificate"><span>Get a signing certificate</span></a></h2>
<p>The first step is to generate a signing keypair, and get a signing
certificate from Apple, which requires you to subscribe to
<a href="https://developer.apple.com/">Apple’s developer program</a>.</p>
<p>Then, follow <a href="https://developer.apple.com/help/account/create-certificates/create-a-certificate-signing-request">create a certificate signing request</a>.</p>
<p>In my experience, it doesn’t seem that the <strong>User Email Address</strong> you
input matters.</p>
<p>As for <strong>Common Name</strong>, it seems to only affect how the private and
public key are named in Keychain Access.</p>
<p>By saving the request to disk, you will get a
<code>CertificateSigningRequest.certSigningRequest</code> file.</p>
<p>This will also create a <code>Common Name.p12</code> and <code>Common Name.pem</code> entry in
your Keychain Access. The <code>.p12</code> is the private key, and the <code>.pem</code> is
the public key, that are going to be associated with the certificate
you’re requesting.</p>
<p>You should now upload the <code>.certSigningRequest</code> file to your Apple
Developer account, in <strong>Certificates, IDs &amp; Profiles</strong>. Choose the
<strong>Developer ID Application</strong> certificate type.</p>
<p>This will give you a certificate <code>developerID_application.cer</code> that you
need to import in Keychain Access (by simply opening it).</p>
<div class="note">
<p><strong>Note:</strong> if it refuses to open with “The System Roots keychain cannot
be modified”, it’s because Keychain Access opens by default in the
System Roots keychain and you can only update it as superuser (so mainly
from the CLI with <code>sudo security ...</code>).</p>
<p>We only need this certificate in the login keychain, so make sure it’s
the selected one in Keychain Access and then open the certificate again.</p>
</div>
<h2 id="get-the-intermediate-certificate" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#get-the-intermediate-certificate"><span>Get the intermediate certificate</span></a></h2>
<p>In Apple’s <abbr title="Public key infrastructure"><a href="https://www.apple.com/certificateauthority/">PKI</a></abbr>,
your developer certificate is signed by an intermediate certificate,
that’s itself signed by one of Apple’s root certificates.</p>
<p>The Apple root certificates should already be present out of the box in
your System Roots keychain, but the intermediate ones are not, and need
to be downloaded for the certificate chain to be complete and for
<code>codesign</code> to work.</p>
<p>When you created your Developer ID certificate, you were likely prompted
between “G2 Sub-CA” and “Previous Sub-CA”. At the time of writing, on the
<a href="https://www.apple.com/certificateauthority/">Apple PKI page</a>,
those are respectively <a href="https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer">Developer ID - G2 (Expiring 09/17/2031 00:00:00 UTC)</a>
and <a href="https://www.apple.com/certificateauthority/DeveloperIDCA.cer">Developer ID - G1 (Expiring 02/01/2027 22:12:15 UTC)</a>.
So go ahead and download the appropriate one and install it to your
login keychain just like your developer certificate.</p>
<div class="note">
<p><strong>Note:</strong> if you don’t know what intermediate certificate you need, you
can see what <abbr title="Certificate authorith">CA</abbr> signed your
certificate like so:</p>
<pre><code class="hljs language-console"><span class="hljs-meta prompt_">$ </span><span class="language-bash">openssl x509 -<span class="hljs-keyword">in</span> <span class="hljs-string">&quot;developerID_application.cer&quot;</span> -noout -issuer</span>
issuer=CN=Developer ID Certification Authority, OU=G2, O=Apple Inc., C=US
</code></pre>
<p>In this case, you can see my certificate was issued using the G2
intermediate certificate.</p>
</div>
<div class="note warn">
<p><strong>Warning:</strong> <a href="https://developer.apple.com/forums/thread/86161?answerId=422698022#422698022">leave the certificate trust settings</a>
to “Use System Defaults” and do not mark it as “Always Trust”, both for
your developer certificate and the intermediate certificate.</p>
<p>If like me you were getting issues signing things and assumed marking as
“Always Trust” could help, beware, it does the opposite and prevents
<code>codesign</code> from working altogether for some reason, even after you fix
the actual cause of your signing issues. 😅</p>
<p>Whether you’re missing a certificate, or if you have the right
certificates but they’re marked as “Always Trust”, you’ll get that same
error:</p>
<pre><code class="hljs">Warning: unable to build chain to self-signed root for signer
</code></pre>
</div>
<h2 id="manually-sign-your-app" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#manually-sign-your-app"><span>Manually sign your app</span></a></h2>
<p>You’re now in a place where you can manually sign your app:</p>
<pre><code class="hljs language-sh">codesign --sign <span class="hljs-string">&#x27;Developer ID Application: MyApp (ID)&#x27;</span> MyApp.app
</code></pre>
<p>To find the identify to pass to <code>--sign</code>:</p>
<pre><code class="hljs language-sh">security find-identity -v -p codesigning
</code></pre>
<p><code>-v</code> will show only valid identities. <code>-p</code> is for selecting a specific
policy, here we care about <code>codesigning</code>.</p>
<h2 id="signing-your-app-with-electron-forge" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#signing-your-app-with-electron-forge"><span>Signing your app with Electron Forge</span></a></h2>
<p>Add <code>osxSign</code> to your <code>forge.config.js</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-attr">packagerConfig</span>: {
    <span class="hljs-attr">osxSign</span>: {
      <span class="hljs-attr">identity</span>: <span class="hljs-string">&#x27;Developer ID Application: MyApp (ID)&#x27;</span>
    }
  }
}
</code></pre>
<p>If you only have one valid code signing identity configured on your Mac,
you can omit the <code>identity</code> parameter. You still need to pass an empty
object <code>osxSign: {}</code>.</p>
<h2 id="notarizing-your-app-with-electron-forge" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#notarizing-your-app-with-electron-forge"><span>Notarizing your app with Electron Forge</span></a></h2>
<p>Add <code>osxNotarize</code> to your <code>forge.config.js</code>. There’s a few ways to
configure it <a href="https://www.electronforge.io/guides/code-signing/code-signing-macos#osxnotarize-options">documented here</a>.</p>
<p>The documentation is pretty clear and complete so I won’t bother
repeating anything here. 🙂</p>
<h2 id="debugging-osxsign-and-osxnotarize" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#debugging-osxsign-and-osxnotarize"><span>Debugging <code>osxSign</code> and <code>osxNotarize</code></span></a></h2>
<p>If you encounter issues where Electron is not properly signing or
notarizing your app, you can debug the signing and notarizing process
that way:</p>
<pre><code class="hljs language-sh">DEBUG=electron-osx-sign,electron-notarize* npx electron-forge package
</code></pre>
<p>This will output detailed logs that should help you identify the
culprit.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html#conclusion"><span>Conclusion</span></a></h2>
<p>That should be all you need to have your app approved by Apple so that
you can share it with the world. 🌎</p>
<p>Happy building! 🚀</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Join the discussion on <a href="https://x.com/valeriangalliat/status/1790906279852204042">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>How to use Electron auto updater ⚛️</title>
    <link href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html" />
    <id>https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html</id>
    <updated>2024-05-15T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>I’m <a href="https://www.arcade.software/download">writing an Electron app for the first time</a>,
and I was wondering how to make it auto update. Turns out it’s
relatively easy, but I found a ton of conflicting documentations about
it and was quite confused for a while, which is why I’m writing this
post.</p>
<p>In this post, I’m gonna focus on a macOS app. I’m not sure how much of
this applies to Windows. I’ll update this post when we eventually port
the app to Windows!</p>
<h2 id="how-does-the-auto-updater-works" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#how-does-the-auto-updater-works"><span>How does the auto updater works?</span></a></h2>
<p>On macOS, Electron auto updater uses the <a href="https://github.com/Squirrel/Squirrel.Mac">Squirrel.Mac</a>
framework, a “Cocoa framework for updating macOS apps”.</p>
<p>So ultimately, when it comes to the distribution of auto updates, your
source of truth is gonna be Squirrel.</p>
<p>Electron has a <a href="https://www.electronjs.org/docs/latest/api/auto-updater">built-in <code>autoUpdater</code> API</a>
that lets you <a href="https://www.electronjs.org/docs/latest/api/auto-updater#autoupdatersetfeedurloptions"><code>setFeedURL</code></a>,
<a href="https://www.electronjs.org/docs/latest/api/auto-updater#autoupdatercheckforupdates"><code>checkForUpdates</code></a>,
and <a href="https://www.electronjs.org/docs/latest/api/auto-updater#autoupdaterquitandinstall"><code>quitAndInstall</code></a>.</p>
<p>On boot, you configure the auto updater with a mysterious, undocumented
“feed URL”, and then you check for updates periodically, and when an
update is found, you can prompt the user to install the update.</p>
<div class="note">
<p><strong>Note:</strong> for auto updates to work, your releases must be
<a href="https://www.codejam.info/2024/05/sign-notarize-electron-app-macos.html">signed</a>.</p>
</div>
<h2 id="what-about-update-electron-app" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#what-about-update-electron-app"><span>What about <code>update-electron-app</code>?</span></a></h2>
<p>If you read the <a href="https://www.electronjs.org/docs/latest/tutorial/updates">Electron reference on updating applications</a>,
they mention a <a href="https://github.com/electron/update-electron-app"><code>update-electron-app</code></a>
package, that identifies as “a drop-in module that adds auto updating
capabilities to Electron apps”.</p>
<p>This module implements the logic described in the previous section for
you, so you just have to call one function on boot and let it deal with
periodic checking, and prompting the user to install the update. Cool.</p>
<p>However it’s only meant to <a href="https://github.com/electron/update-electron-app?tab=readme-ov-file#with-updateelectronjsorg">work with Electron’s public update service</a>,
or static file storage that <a href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#static-updates-format">we’ll talk about later</a></p>
<p>The typical usage looks like this when using Electron’s public update
service:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { updateElectronApp, <span class="hljs-title class_">UpdateSourceType</span> } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;update-electron-app&#x27;</span>)

<span class="hljs-title function_">updateElectronApp</span>({
  <span class="hljs-attr">updateSource</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-title class_">UpdateSourceType</span>.<span class="hljs-property">ElectronPublicUpdateService</span>,
    <span class="hljs-attr">repo</span>: <span class="hljs-string">&#x27;github-user/repo&#x27;</span>
  }
})
</code></pre>
<h2 id="using-update-electronjs-org" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#using-update-electronjs-org"><span>Using <code>update.electronjs.org</code>?</span></a></h2>
<p>That public update service is hosted by Electron and serves the obscure
“feed URL” that we encountered earlier.</p>
<p>In order to use it, you need to point it to a public GitHub repository
where you publish <a href="https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository">releases</a>
of your app.</p>
<p>Their service can then respond to auto update requests by checking if
there’s a newer release. The app binary is downloaded directly from
GitHub releases.</p>
<p>You can also <a href="https://www.electronjs.org/docs/latest/tutorial/updates#step-1-deploying-an-update-server">host your own update server</a>.
There’s actually a few options you can chose from, and they all comply
to this undocumented feed format we still know nothing about.</p>
<p>When using the <code>autoUpdater</code> module, you can configure it like this:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { autoUpdater } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;electron&#x27;</span>)

autoUpdater.<span class="hljs-title function_">setFeedURL</span>({
  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://server/path/to/feed&#x27;</span>
})
</code></pre>
<p>That URL seems arbitrary and typically contains the <code>process.platform</code>,
maybe <code>process.arch</code>, and your program’s version.</p>
<p>As we saw before, a custom dynamic server won’t work with
<code>update-electron-app</code> so you’ll have to implement the logic yourself.
Luckily, <a href="https://github.com/electron/update-electron-app/blob/515ab245a429a4790b9209f8d2073edddb980717/src/index.ts">it’s not very complicated</a>.</p>
<h2 id="what-s-behind-this-feed-url-and-format" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#what-s-behind-this-feed-url-and-format"><span>What’s behind this feed URL and format?</span></a></h2>
<p>This format is actually <a href="https://github.com/Squirrel/Squirrel.Mac?tab=readme-ov-file#update-requests">defined by the Squirrel framework</a>.</p>
<p>In case of a dynamic server like in the previous section, the request is
as an arbitrary <code>GET</code> request to the URL you configured. It’s important
for that URL to include the current app version because your server is
expected to respond based on whether or not a new version is available
for the given version.</p>
<p>In case no update is available, you <a href="https://github.com/Squirrel/Squirrel.Mac?tab=readme-ov-file#server-support">should return</a>
a <code>204 No Content</code>.</p>
<p>If an update is available, you should return a <code>200 OK</code> with the
<a href="https://github.com/Squirrel/Squirrel.Mac?tab=readme-ov-file#update-server-json-format">following</a>
JSON response:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://server/path/to/release.zip&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional Release Name&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;notes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional release notes&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;pub_date&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2024-05-03T12:34:56Z&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>Now this makes a bit more sense. You can easily make your own server
that implements this protocol. Actually, you can probably get away with
adding just another endpoint to your existing app. 😎 No need to depend
on a third-party service or to self-host and maintain another app. 😅</p>
<h2 id="static-updates-format" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#static-updates-format"><span>Static updates format</span></a></h2>
<p>What’s a bit lesser known is that you don’t even need a dynamic server
at all. You can implement auto updates with static files only. 🪶</p>
<p>There’s hints of that in <code>update-electron-app</code> that has a
<a href="https://github.com/electron/update-electron-app/tree/main?tab=readme-ov-file#with-static-file-storage">static storage option</a>,
as well as Squirrel’s docs that mention a <a href="https://github.com/Squirrel/Squirrel.Mac?tab=readme-ov-file#update-file-json-format">static JSON format</a>.</p>
<p>With <code>update-electron-app</code>, it would look like this:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { updateElectronApp, <span class="hljs-title class_">UpdateSourceType</span> } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;update-electron-app&#x27;</span>)

<span class="hljs-title function_">updateElectronApp</span>({
  <span class="hljs-attr">updateSource</span>: {
    <span class="hljs-attr">type</span>: <span class="hljs-title class_">UpdateSourceType</span>.<span class="hljs-property">StaticStorage</span>,
    <span class="hljs-attr">baseUrl</span>: <span class="hljs-string">&#x27;https://server/path/to/feed&#x27;</span>
  }
})
</code></pre>
<div class="note">
<p><strong>Note:</strong> when using <code>update-electron-app</code>, on macOS, it will
<a href="https://github.com/electron/update-electron-app/blob/515ab245a429a4790b9209f8d2073edddb980717/src/index.ts#L121">append</a>
<code>/RELEASES.json</code> to the <code>baseUrl</code> URL that you give when in
<code>StaticStorage</code> mode, meaning in the above example, the final URL would
be <code>https://server/path/to/feed/RELEASES.json</code>.</p>
<p>There’s no way to opt out of that, so if you’re gonna use this module,
that’s something to know when you create the layout of your static file
storage. Luckily the <a href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#auto-generating-the-static-update-files">automated way to provision static updates with Electron Forge</a>
generates a <code>RELEASES.json</code> file by default so it should work out of the
box.</p>
</div>
<p>As for the native <code>autoUpdater</code>, you need to pass the
<a href="https://www.electronjs.org/docs/latest/api/auto-updater#autoupdatersetfeedurloptions">little documented <code>serverType: 'json'</code></a>:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { autoUpdater } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;electron&#x27;</span>)

autoUpdater.<span class="hljs-title function_">setFeedURL</span>({
  <span class="hljs-attr">url</span>: <span class="hljs-string">&#x27;https://server/path/to/feed.json&#x27;</span>,
  <span class="hljs-attr">serverType</span>: <span class="hljs-string">&#x27;json&#x27;</span>
})
</code></pre>
<p>In both cases, the feed URL typically contains <code>process.platofrm</code> and maybe <code>process.arch</code> again, but that seems to be really up to you.</p>
<p>It is supposed to respond with the following <a href="https://github.com/Squirrel/Squirrel.Mac?tab=readme-ov-file#update-file-json-format">schema</a>:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;currentRelease&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;releases&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.1&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;updateTo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.1&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://server/path/to/1.2.1.zip&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional Release Name&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;notes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional release notes&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;pub_date&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2024-05-02T12:34:56Z&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;updateTo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://server/path/to/1.2.3.zip&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional Release Name&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;notes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional release notes&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;pub_date&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2024-05-03T12:34:56Z&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>From this static response, Squirrel is able to determine whether it
needs to update, and where to fetch the update from.</p>
<p>Don’t get confused by the <code>updateTo</code> naming. <code>releases</code> contains all the
releases of your software, and <code>updateTo</code> just contains some metadata
about that release, with the <code>url</code> being the only really important part.</p>
<p>I haven’t tested this, but my guess is that all you really need is the
entry containing the <code>currentRelease</code>, e.g.:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;currentRelease&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;releases&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;updateTo&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">&quot;version&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1.2.3&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;url&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://server/path/to/1.2.3.zip&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;name&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional Release Name&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;notes&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;Optional release notes&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">&quot;pub_date&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;2024-05-03T12:34:56Z&quot;</span>
      <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>That should be enough for Squirrel to know there’s an update available.
I’m not sure keeping the entire history of older releases adds any value.</p>
<h2 id="auto-generating-the-static-update-files" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#auto-generating-the-static-update-files"><span>Auto generating the static update files</span></a></h2>
<p>From the above section, you should have everything you need to manually
craft that updates feed and push it on your static file server with your
ZIP updates.</p>
<p>However, if you use <a href="https://www.electronforge.io/">Electron Forge</a>,
there’s (again little documented) ways to generate this static structure
automatically!</p>
<p><code>update-electron-app</code> <a href="https://github.com/electron/update-electron-app/tree/main?tab=readme-ov-file#requirements">hints</a>
at <a href="https://www.electronforge.io/config/publishers/s3"><code>@electron-forge/publisher-s3</code></a>,
but there’s also <a href="https://www.electronforge.io/config/publishers/gcs"><code>@electron-forge/publisher-gcs</code></a>,
allowing you to generate and upload that static update structure
respectively to AWS S3 or Google Cloud Storage.</p>
<p>They both work the same but the documentation of the S3 plugin is more
complete when it comes to <a href="https://www.electronforge.io/config/publishers/s3#auto-updating-from-s3">auto updating</a>.</p>
<p>You need not only to add the S3 or GCS publisher, but also configure
<a href="https://www.electronforge.io/config/makers/zip"><code>@electron-forge/maker-zip</code></a>
with the undocumented option <code>macUpdateManifestBaseUrl</code>.</p>
<p>During the “make” step, Electron will build the ZIP file for the
release, but with that option, it will also fetch your current static
“update feed”, update the <code>currentRelease</code>, and add a new release entry
to the <code>releases</code> array, then output that updated <code>RELEASES.json</code> file
next to your ZIP files.</p>
<p>Then the S3 or GCS publisher will know to put that new update feed in
the right place in your bucket.</p>
<p>In <code>forge.config.js</code>, it looks like this:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-attr">makers</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;@electron-forge/maker-zip&#x27;</span>,
      <span class="hljs-attr">config</span>: <span class="hljs-function"><span class="hljs-params">arch</span> =&gt;</span> ({
        <span class="hljs-attr">macUpdateManifestBaseUrl</span>: <span class="hljs-string">`https://my-bucket.s3.amazonaws.com/custom/folder/darwin/<span class="hljs-subst">${arch}</span>`</span>
      })
    }
  ],
  <span class="hljs-attr">publishers</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;@electron-forge/publisher-s3&#x27;</span>,
      <span class="hljs-attr">config</span>: {
        <span class="hljs-attr">bucket</span>: <span class="hljs-string">&#x27;my-bucket&#x27;</span>,
        <span class="hljs-attr">folder</span>: <span class="hljs-string">&#x27;custom/folder&#x27;</span>,
        <span class="hljs-attr">public</span>: <span class="hljs-literal">true</span>
      }
    }
    <span class="hljs-comment">// {</span>
    <span class="hljs-comment">//   name: &#x27;@electron-forge/publisher-gcs&#x27;,</span>
    <span class="hljs-comment">//   config: {</span>
    <span class="hljs-comment">//     bucket: &#x27;my-bucket&#x27;,</span>
    <span class="hljs-comment">//     folder: &#x27;custom/folder&#x27;,</span>
    <span class="hljs-comment">//     public: true</span>
    <span class="hljs-comment">//   }</span>
    <span class="hljs-comment">// }</span>
  ]
}
</code></pre>
<p>In the case of <code>macUpdateManifestBaseUrl</code>, like for
<code>update-electron-app</code> in JSON mode, it will <a href="https://github.com/electron/forge/blob/ce2b03934ecf600525366a252e5bcb5491708a27/packages/maker/zip/src/MakerZIP.ts#L50">automatically append</a>
<code>/RELEASES.json</code>, so in the above example, if <code>arch</code> is <code>arm64</code>, the
complete feed URL would be <code>https://my-bucket.s3.amazonaws.com/custom/folder/darwin/arm64/RELEASES.json</code>.</p>
<div class="note">
<p><strong>Note:</strong> if you’re doing universal builds by running <code>electron-forge package --arch universal</code>, then the <code>arch</code> path component will be
<code>universal</code>, so in the above example, you would need to configure
<code>@electron-forge/maker-zip</code> like this:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-attr">makers</span>: [
    {
      <span class="hljs-attr">name</span>: <span class="hljs-string">&#x27;@electron-forge/maker-zip&#x27;</span>,
      <span class="hljs-attr">config</span>: <span class="hljs-function">() =&gt;</span> ({
        <span class="hljs-attr">macUpdateManifestBaseUrl</span>: <span class="hljs-string">&#x27;https://my-bucket.s3.amazonaws.com/custom/folder/darwin/universal&#x27;</span>
      })
    }
  ]
}
</code></pre>
</div>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/how-to-use-electron-auto-updater.html#conclusion"><span>Conclusion</span></a></h2>
<p>Will you use Electron’s hosted update service? Or self-host an
open-source update server? Or instead implement your own dynamic
endpoint? Or maybe you’ll just push static updates on S3, GCS, or your
own file server?</p>
<p>Regardless what you chose, you should now have all the elements you need
to implement auto updates in your Electron app on macOS the way that
suits you best! Cheers. ✌️</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Join the discussion on <a href="https://x.com/valeriangalliat/status/1790906278086386079">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Invoking a Firebase callable function from the Firebase Admin SDK</title>
    <link href="https://www.codejam.info/2024/05/firebase-callable-admin-sdk.html" />
    <id>https://www.codejam.info/2024/05/firebase-callable-admin-sdk.html</id>
    <updated>2024-05-15T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p><a href="https://stackoverflow.com/a/65061421/4324668">LOL you can’t</a>.</p>
<p>What you <a href="https://stackoverflow.com/a/65062301/4324668"><em>can</em></a> do
however is using the Firebase Admin SDK to create a custom token for the
client SDK, and use the client SDK to make the call. 🙃</p>
<p>What does this looks like?</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> admin = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase-admin&#x27;</span>)
<span class="hljs-keyword">const</span> { initializeApp } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase/app&#x27;</span>)
<span class="hljs-keyword">const</span> { getAuth, signInWithCustomToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase/auth&#x27;</span>)
<span class="hljs-keyword">const</span> { getFunctions, httpsCallable } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase/auth&#x27;</span>)

admin.<span class="hljs-title function_">initializeApp</span>({
  <span class="hljs-comment">// Your admin config</span>
})

<span class="hljs-title function_">initializeApp</span>({
  <span class="hljs-comment">// Your client config</span>
})

<span class="hljs-keyword">const</span> token = <span class="hljs-keyword">await</span> admin.<span class="hljs-title function_">auth</span>().<span class="hljs-title function_">createCustomToken</span>(<span class="hljs-string">&#x27;admin&#x27;</span>)

<span class="hljs-keyword">await</span> <span class="hljs-title function_">signInWithCustomToken</span>(<span class="hljs-title function_">getAuth</span>(), token)

<span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> <span class="hljs-title function_">httpsCallable</span>(<span class="hljs-title function_">getFunctions</span>(), <span class="hljs-string">&#x27;myCallableFunction&#x27;</span>).<span class="hljs-title function_">call</span>({})
</code></pre>
<p>Here we created a custom token for a virtual user with UID <code>admin</code> (it
doesn’t need to exist in Firebase Auth). We can verify that in the
function:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> functions = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase-functions&#x27;</span>)

<span class="hljs-built_in">exports</span>.<span class="hljs-property">myCallableFunction</span> = functions.<span class="hljs-property">https</span>.<span class="hljs-title function_">onCall</span>(<span class="hljs-title function_">async</span> (data, context) =&gt; {
  <span class="hljs-keyword">if</span> (!context.<span class="hljs-property">auth</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> functions.<span class="hljs-property">https</span>.<span class="hljs-title class_">HttpsError</span>(
      <span class="hljs-string">&#x27;unauthenticated&#x27;</span>,
      <span class="hljs-string">&#x27;User is not authenticated&#x27;</span>
    )
  }

  <span class="hljs-keyword">if</span> (context.<span class="hljs-property">auth</span>.<span class="hljs-property">uid</span> !== <span class="hljs-string">&#x27;admin&#x27;</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> functions.<span class="hljs-property">https</span>.<span class="hljs-title class_">HttpsError</span>(
      <span class="hljs-string">&#x27;permission-denied&#x27;</span>,
      <span class="hljs-string">&#x27;User is not authorized&#x27;</span>
    )
  }
})
</code></pre>
<h2 id="why-callable-instead-of-http-function" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/firebase-callable-admin-sdk.html#why-callable-instead-of-http-function"><span>Why callable instead of HTTP function?</span></a></h2>
<p>With a <a href="https://firebase.google.com/docs/functions/http-events">HTTP function</a>,
we could have made a simple <code>fetch</code> request to the endpoint.</p>
<p>Why use a <a href="https://firebase.google.com/docs/functions/callable">callable function</a>
then?</p>
<p>In my case, it was because callable functions have auth built-in,
whereas you’re responsible to implement your own auth for HTTP
functions. I found that using a <code>https.onCall</code> function with a custom
Firebase token was more elegant than configuring some kind of internal
“API key”.</p>
<h2 id="invoking-the-callable-function-manually" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/firebase-callable-admin-sdk.html#invoking-the-callable-function-manually"><span>Invoking the callable function manually</span></a></h2>
<p>It turns out it’s also quite easy to invoke a callable function without
the Firebase SDK, via a <a href="https://firebase.google.com/docs/functions/callable-reference">plain HTTP call</a>.</p>
<p>With cURL, calling a function that doesn’t have token authentication is
as simple as:</p>
<pre><code class="hljs language-sh">curl \
    -X POST \
    -H <span class="hljs-string">&#x27;Content-Type: application/json&#x27;</span> \
    <span class="hljs-string">&#x27;https://region-project.cloudfunctions.net/myCallableFunction&#x27;</span> \
    --data <span class="hljs-string">&#x27;{&quot;data&quot;: {}}&#x27;</span>
</code></pre>
<p>For the authentication, we need an <code>Authorization: Bearer</code> header, but
we can’t directly use the custom token we generated above. We need to
<a href="https://stackoverflow.com/a/51346783/4324668">exchange</a> it for an ID
token first (this happened transparently in the previous example).</p>
<p>We could use the client SDK to do that for us but at that point we might
as well use the client SDK to call the function as well. 😅</p>
<p>Just for educational purpose, and building off the earlier example, it
would look like:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> { getAuth, signInWithCustomToken, getIdToken } = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;firebase/auth&#x27;</span>)

<span class="hljs-keyword">await</span> <span class="hljs-title function_">signInWithCustomToken</span>(<span class="hljs-title function_">getAuth</span>(), token)

<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-keyword">await</span> <span class="hljs-title function_">getIdToken</span>(<span class="hljs-title function_">getAuth</span>().<span class="hljs-property">currentUser</span>))
</code></pre>
<p>We could then use that token in the cURL request:</p>
<pre><code class="hljs language-sh">curl \
    -X POST \
    -H <span class="hljs-string">&#x27;Content-Type: application/json&#x27;</span> \
    -H <span class="hljs-string">&quot;Authorization: Bearer <span class="hljs-variable">$token</span>&quot;</span> \
    <span class="hljs-string">&#x27;https://region-project.cloudfunctions.net/myCallableFunction&#x27;</span> \
    --data <span class="hljs-string">&#x27;{&quot;data&quot;: {}}&#x27;</span>
</code></pre>
<p>But if we’re calling the function via <code>fetch</code>, it’s probably that we
don’t want to use the client SDK. Then, exchanging the token would
look like <a href="https://cloud.google.com/identity-platform/docs/use-rest-api#section-verify-custom-token">this</a>:</p>
<pre><code class="hljs language-sh">curl \
    -X POST \
    -H <span class="hljs-string">&#x27;Content-Type: application/json&#x27;</span> \
    <span class="hljs-string">&quot;https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=<span class="hljs-variable">$firebaseApiKey</span>&quot;</span> \
    --data <span class="hljs-string">&quot;{\&quot;token\&quot;: \&quot;<span class="hljs-variable">$customToken</span>\&quot;, \&quot;returnSecureToken\&quot;: true}&quot;</span>
</code></pre>
<p>This returns an <code>idToken</code> that we can use as <code>Authorization: Bearer</code> in
the invocation of the callable function as seen above.</p>
<div class="note">
<p><strong>Note:</strong> if you’re wondering about <code>returnSecureToken</code>, it’s
<a href="https://cloud.google.com/identity-platform/docs/use-rest-api#section-verify-custom-token">documented</a>
as “should always be true”.</p>
<p>Without it, the endpoint returns only an <code>idToken</code> with no <code>expiresIn</code>
nor <code>refreshToken</code>, so my guess is that it’s a token that… doesn’t
expire? Which is considered insecure.</p>
</div>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Jest and Firestore: could not reach Cloud Firestore backend</title>
    <link href="https://www.codejam.info/2024/05/jest-firestore-could-not-reach-firestore-backend.html" />
    <id>https://www.codejam.info/2024/05/jest-firestore-could-not-reach-firestore-backend.html</id>
    <updated>2024-05-08T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>So you’re using Jest to do some unit tests that involve testing
Firebase-related stuff like Firestore, maybe Firestore rules with
<a href="https://firebase.google.com/docs/rules/unit-tests"><code>@firebase/rules-unit-testing</code></a>?</p>
<p>But your test just times out:</p>
<pre><code class="hljs">thrown: &quot;Exceeded timeout of 5000 ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout.&quot;
</code></pre>
<p>So you go on and add a longer timeout value to the test, but then you
hit another level of timeout:</p>
<pre><code class="hljs">@firebase/firestore: Firestore: Could not reach Cloud Firestore backend. Backend didn&#x27;t respond within 10 seconds.
This typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.
</code></pre>
<p>By any chance, are you using <a href="https://jestjs.io/docs/next/tutorial-jquery"><code>jest-environment-jsdom</code></a>?
Something like this in your <code>jest.config.js</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-attr">testEnvironment</span>: <span class="hljs-string">&#x27;jsdom&#x27;</span>
  <span class="hljs-comment">// testEnvironment: &#x27;jest-environment-jsdom&#x27;</span>
}
</code></pre>
<p>If so, look no further. Firestore doesn’t like what
<code>jest-environment-jsdom</code> does to the global object and makes it hang
forever.</p>
<p>It took me long enough to figure <em>that</em> out, so I didn’t manage to
figure out <em>why</em> exactly it’s the case. So far my understanding is that
it’s related to the <code>fetch</code> API <em>somehow</em>, because if you set the
undocumented <code>useFetchStreams</code> option to <code>false</code> in the Firebase client,
then it falls back to <code>XMLHttpRequest</code> (which jsdom implements) and
things work again.</p>
<pre><code class="hljs language-js">user.<span class="hljs-title function_">firestore</span>({ <span class="hljs-attr">useFetchStreams</span>: <span class="hljs-literal">false</span>, <span class="hljs-attr">merge</span>: <span class="hljs-literal">true</span> })
</code></pre>
<p>My advice would be to run the Firestore tests in the default Node.js
environment instead of the jsdom environment. This may be by using a
dedicated <code>jest.config.js</code>, or simply running your Firestore tests
separately from the rest of your frontend test suite and passing <code>--env node</code> to override the value from <code>jest.config.js</code>:</p>
<pre><code class="hljs language-sh">npx jest --<span class="hljs-built_in">env</span> node firestore.test.js
</code></pre>
<p>Last resort, the <code>useFetchStreams</code> hack above should do it. 😄</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Using Google Chrome instead of Chromium in Google Cloud Functions</title>
    <link href="https://www.codejam.info/2024/05/google-chrome-cloud-functions.html" />
    <id>https://www.codejam.info/2024/05/google-chrome-cloud-functions.html</id>
    <updated>2024-05-05T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>When using Puppeteer, Playwright and similar, you need to have Chrome
installed. When you’re running on AWS Lambda or Google Cloud Functions,
it can get tricky.</p>
<p>Google Cloud Functions <em>used to</em> bundle Chromium in their base images,
but it’s been a few years it’s no longer the case. That’s where packages
like <a href="https://github.com/alixaxel/chrome-aws-lambda"><code>chrome-aws-lambda</code></a>
come in handy, by bundling Chromium directly inside a npm package, and
exposing a function that extracts the Chromium binary and returns the
path:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> chromium = <span class="hljs-built_in">require</span>(<span class="hljs-string">&#x27;chrome-aws-lambda&#x27;</span>)

<span class="hljs-keyword">const</span> path = <span class="hljs-keyword">await</span> chromium.<span class="hljs-property">executablePath</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> unnecessary pedantic detail: the above code doesn’t look like a function,
but <a href="https://github.com/alixaxel/chrome-aws-lambda/blob/f9d5a9ff0282ef8e172a29d6d077efc468ca3c76/source/index.ts#L147">it is, in fact</a>,
a getter function that returns a promise. 😄</p>
</div>
<p>However that’s Chromium, and you may have reasons to want Google Chrome
instead (mainly, proprietary codecs).</p>
<h2 id="a-totally-unrelated-note-about-aws-lambda" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/google-chrome-cloud-functions.html#a-totally-unrelated-note-about-aws-lambda"><span>A totally unrelated note about AWS Lambda</span></a></h2>
<p>This article is about Google Cloud Functions, but if you’re on AWS
Lambda, the above option is your best bet. Because of the Lambda total
size limit of 250 MB (all layers combined), it’s really hard to get a
binary of Chrome that fits in there.</p>
<p>That’s why <code>chrome-aws-lambda</code> uses <a href="https://github.com/alixaxel/lambdafs">LambdaFS</a>
under the hood, to aggressively compress the Chrome installation with
Brotli and make it fit in that limited space.</p>
<p>But again with that build, you won’t have proprietary codecs. I tried to
trim down a Chrome Linux build and compress it with the same technique
but never managed to make it fit on AWS Lambda. Recent Chrome versions
are just too big.</p>
<p>There’s another option, which is to compile Chromium yourself with
proprietary codecs. I never found any prebuilt binaries of Chromium that
include proprietary codecs (maybe because of license issues
redistributing them 🙃) so you’re on your own here.</p>
<p><a href="https://www.remotion.dev/">Remotion</a> successfully does that for
<a href="https://www.remotion.dev/docs/lambda">Remotion Lambda</a>.
Here’s <a href="https://github.com/remotion-dev/chrome-build-instructions">their instructions</a>
to compile Chromium with proprietary codecs for Lambda.</p>
<p>Fair warning: it gets hairy, fast.</p>
<h2 id="back-to-google-cloud-functions" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/google-chrome-cloud-functions.html#back-to-google-cloud-functions"><span>Back to Google Cloud Functions</span></a></h2>
<p>Google Cloud Functions is more generous as for bundle size, so we don’t
need to resort to those tricks, and we can include a complete,
uncompressed, Google Chrome installation.</p>
<p>Google publishes <a href="https://googlechromelabs.github.io/chrome-for-testing/">Chrome for Testing</a>,
builds <a href="https://developer.chrome.com/blog/chrome-for-testing">specifically made</a>
for headless usage.</p>
<p>We can just download the latest build from there as part of the
<code>gcp-build</code> script in our <code>package.json</code>.</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;gcp-build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;curl -s -O &#x27;https://storage.googleapis.com/chrome-for-testing-public/124.0.6367.91/linux64/chrome-linux64.zip&#x27; &amp;&amp; unzip chrome-linux64.zip &amp;&amp; rm chrome-linux64.zip&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> the <code>gcp-build</code> script allows you to <a href="https://cloud.google.com/appengine/docs/standard/nodejs/running-custom-build-step">run a custom build step</a>
in Google Cloud Build, which is what Cloud Functions (both 1st and 2nd
gen, as well as Cloud Run and App Engine) use to build your function
image.</p>
<p>It would work just fine with a <code>postinstall</code> script as well, but
<code>gcp-build</code> makes sure you run it only on Google Cloud Build, which is
probably desirable in this particular case.</p>
</div>
<p>You will then have the Chrome binary in <code>chrome-linux64/chrome</code>, that
you can pass to the tool of your choice.</p>
<h2 id="with-puppeteer" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/google-chrome-cloud-functions.html#with-puppeteer"><span>With Puppeteer</span></a></h2>
<p>Courtesy of <a href="https://medium.com/@jackklpan/run-puppeteer-in-google-cloud-functions-v2-b18a353e609b">this post</a>,
with Puppeteer, you don’t need to download Chrome manually, since it
provides a nifty script to do just that.</p>
<p>Actually, Puppeteer’s <a href="https://github.com/puppeteer/puppeteer/blob/f23646b3526aa87145c17b22e9967ec8f77d82d2/packages/puppeteer/package.json#L41"><code>postinstall</code> script</a>
automatically downloads the latest version of Chrome for Testing for
your platform.</p>
<p>The caveat is that this script by default installs it to
<code>~/.cache/puppeteer</code>, which in the case of Google Cloud Build, is not
gonna be preserved in the final image. So we need to instruct Puppeteer
to install Chrome in a directory that Cloud Build will keep.</p>
<p>This can be done with the following <code>.puppeteerrc.js</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-attr">cacheDirectory</span>: <span class="hljs-string">`<span class="hljs-subst">${__dirname}</span>/.cache/puppeteer`</span>
}
</code></pre>
<p>But even then, there’s another caveat. Puppeteer’s <code>postinstall</code> script
will only run after it gets installed. However, because of build
caching, you will get in a state where <code>node_modules</code> is restored, with
Puppeteer already installed (so <code>postinstall</code> will <em>not</em> run), but the
<code>.cache/puppeteer</code> directory will also <em>not</em> be restored.</p>
<p>To mitigate that, we need to make sure to install Chrome systematically.
Again we can leverage the <code>gcp-build</code> for that:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;scripts&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;gcp-build&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;npx puppeteer browsers install chrome&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<div class="note">
<p><strong>Note:</strong> you could call Puppeteer’s <code>postinstall</code> script directly by
doing <code>node node_modules/puppeteer/install.mjs</code> instead, but I found the
above command cleaner.</p>
</div>
<p>The good thing is that this script knows to not re-download Chrome if
it’s already found in the cache directory, so when the <code>postinstall</code>
script <em>does</em> run, the extra <code>gcp-build</code> command will be a no-op.</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Knex: timeout acquiring a connection, the pool is probably full</title>
    <link href="https://www.codejam.info/2024/05/knex-timeout-pool-full.html" />
    <id>https://www.codejam.info/2024/05/knex-timeout-pool-full.html</id>
    <updated>2024-05-05T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<pre><code class="hljs">Error: Knex: Timeout acquiring a connection. The pool is probably full. Are you missing a .transacting(trx) call?
</code></pre>
<p>Yes, you’re probably missing a <code>.transacting(trx)</code> call, but what’s
going on exactly?</p>
<p>Knex maintains a connection pool to your database, which you configure
with the <a href="https://knexjs.org/guide/#pool"><code>pool</code> <code>min</code> and <code>max</code> options</a>.
If <code>max</code> is 5, then Knex will keep up to 5 connections to your database
in the pool.</p>
<p>If you’re attempting to make a query and the pool is full, then it’ll
wait that one of the connection frees up in order to use it.</p>
<p>Sometimes however, it will timeout doing so. One common case is a
deadlock when mixing queries inside and outside a transaction.</p>
<p>Let’s say that you start 5 transactions, but within those transactions,
you’re performing some queries <em>outside</em> of the transaction:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">await</span> knex.<span class="hljs-title function_">transaction</span>(<span class="hljs-function"><span class="hljs-params">trx</span> =&gt;</span> {
  <span class="hljs-keyword">await</span> trx.<span class="hljs-title function_">raw</span>(<span class="hljs-string">&#x27;...&#x27;</span>)
  <span class="hljs-keyword">await</span> knex.<span class="hljs-title function_">raw</span>(<span class="hljs-string">&#x27;...&#x27;</span>)
})
</code></pre>
<p>Here, the <code>knex.raw</code> statement will execute outside of the transaction
(despite being in the function, because it doesn’t use <code>trx</code>). This
means that it will use its own connection from the pool, on top of the
one <code>knex.transaction</code> already uses.</p>
<p>If you have enough of those running in parallel, you can hit a case
where there’s no available connection to execute the <code>knex.raw</code> bit. So
it’s waiting that a connection frees up. <strong>But no connection get freed up
because all the transactions are waiting for the <code>knex.raw</code> bit to
complete in order to commit!</strong></p>
<p>See the deadlock here?</p>
<p>So the solution is to make sure that all the queries you perform inside
the transaction actually use that transaction. In the above example,
it’s very obvious, but it can get trickier when you call a function that
calls another function that calls a method that makes a query but didn’t
accept a <code>trx</code> parameter and so ends up needing its own connection. 😬</p>
<p>Now you know what to look for. 👀</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Prevent macOS to switch to bluetooth headphones microphone 🎧</title>
    <link href="https://www.codejam.info/2024/05/macos-prevent-bluetooth-headphones-microphone.html" />
    <id>https://www.codejam.info/2024/05/macos-prevent-bluetooth-headphones-microphone.html</id>
    <updated>2024-05-05T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Maybe like me, you have bluetooth headphones such as the Sony WH-1000XM4
that you like because they have great audio, but the built-in microphone
otherwise suck. But you don’t care because you use your MacBook’s
microphone.</p>
<p>Then, maybe also like me, you didn’t even <em>know</em> that it had a built-in
microphone in the first place, and even less that macOS was
automatically switching to that microphone when you connect your
headphones!</p>
<p>Luckily, I didn’t sound like shit on calls for too long, because my
friends quickly told me “bro, ur mic sounds like shit”. 💩</p>
<h2 id="forcing-the-internal-microphone" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/macos-prevent-bluetooth-headphones-microphone.html#forcing-the-internal-microphone"><span>Forcing the internal microphone</span></a></h2>
<p>Now we know what’s wrong, let’s fix it. The idea is that when I connect
my bluetooth headphones, I want the audio output to go to them, but I
don’t want to switch my default microphone.</p>
<p>We can achieve that with the <strong>Audio MIDI Setup</strong> app.</p>
<p>Create an <strong>Aggregate Device</strong> (from the <code>+</code> icon at the bottom-left
corner) that has only one input: your MacBook microphone. Then set this
aggregate device as “default for sound input” from the right click menu.</p>
<figure class="center">
  <img alt="Audio MIDI Setup" srcset="../../img/2024/05/macos-microphone/audio-midi-setup.png 2x">
</figure>
<p>Tada! Now connecting your headphones will leave the aggregate device
alone, meaning you’ll keep using the good microphone that comes with
your laptop, without thinking about it. 👌</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Firebase Auth Admin SDK denied when using application default credentials</title>
    <link href="https://www.codejam.info/2024/05/firebase-auth-admin-denied-application-default.html" />
    <id>https://www.codejam.info/2024/05/firebase-auth-admin-denied-application-default.html</id>
    <updated>2024-05-05T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>If you’re using the Firebase Admin SDK from your development machine
e.g. to run ad hoc scripts, you may have tried to do something like
this:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">import</span> admin <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;firebase-admin&#x27;</span>
<span class="hljs-keyword">import</span> { applicationDefault } <span class="hljs-keyword">from</span> <span class="hljs-string">&#x27;firebase-admin/app&#x27;</span>

admin.<span class="hljs-title function_">initializeApp</span>({
  <span class="hljs-attr">projectId</span>: <span class="hljs-string">&#x27;my-project&#x27;</span>,
  <span class="hljs-attr">credential</span>: <span class="hljs-title function_">applicationDefault</span>()
})

<span class="hljs-keyword">const</span> auth = admin.<span class="hljs-title function_">auth</span>()

<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> auth.<span class="hljs-title function_">getUserByEmail</span>(<span class="hljs-string">&#x27;foo@bar.com&#x27;</span>)

<span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(user)
</code></pre>
<p>After all, it works just fine with other Firebase APIs like Firestore.</p>
<p>But in the above case, you’d be getting the following error (spread onto
lines for readability):</p>
<pre><code class="hljs">FirebaseAuthError: //cloud.google.com/docs/authentication/.

If you are getting this error with curl or similar tools, you may need
to specify &#x27;X-Goog-User-Project&#x27; HTTP header for quota and billing
purposes.

For more information regarding &#x27;X-Goog-User-Project&#x27; header, please
check https://cloud.google.com/apis/docs/system-parameters.

Raw server response:

{
  &quot;error&quot;: {
    &quot;code&quot;: 403,
    &quot;message&quot;: &quot;Your application has authenticated using end user credentials from the Google Cloud SDK or Google Cloud Shell which are not supported by the identitytoolkit.googleapis.com. We recommend configuring the billing/quota_project setting in gcloud or using a service account through the auth/impersonate_service_account setting. For more information about service accounts and how to use them in your application, see https://cloud.google.com/docs/authentication/. If you are getting this error with curl or similar tools, you may need to specify &#x27;X-Goog-User-Project&#x27; HTTP header for quota and billing purposes. For more information regarding &#x27;X-Goog-User-Project&#x27; header, please check https://cloud.google.com/apis/docs/system-parameters.&quot;,
    &quot;errors&quot;: [
      {
        &quot;message&quot;: &quot;Your application has authenticated using end user credentials from the Google Cloud SDK or Google Cloud Shell which are not supported by the identitytoolkit.googleapis.com. We recommend configuring the billing/quota_project setting in gcloud or using a service account through the auth/impersonate_service_account setting. For more information about service accounts and how to use them in your application, see https://cloud.google.com/docs/authentication/. If you are getting this error with curl or similar tools, you may need to specify &#x27;X-Goog-User-Project&#x27; HTTP header for quota and billing purposes. For more information regarding &#x27;X-Goog-User-Project&#x27; header, please check https://cloud.google.com/apis/docs/system-parameters.&quot;,
        &quot;domain&quot;: &quot;usageLimits&quot;,
        &quot;reason&quot;: &quot;accessNotConfigured&quot;,
        &quot;extendedHelp&quot;: &quot;https://console.developers.google.com&quot;
      }
    ],
    &quot;status&quot;: &quot;PERMISSION_DENIED&quot;,
    &quot;details&quot;: [
      {
        &quot;@type&quot;: &quot;type.googleapis.com/google.rpc.ErrorInfo&quot;,
        &quot;reason&quot;: &quot;SERVICE_DISABLED&quot;,
        &quot;domain&quot;: &quot;googleapis.com&quot;,
        &quot;metadata&quot;: {
          &quot;service&quot;: &quot;identitytoolkit.googleapis.com&quot;,
          &quot;consumer&quot;: &quot;projects/123456&quot;
        }
      }
    ]
  }
}
}
</code></pre>
<p>So what’s going on? Well <code>applicationDefault()</code> works with the
application default credentials as created by
<a href="https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login"><code>gcloud auth application-default login</code></a>,
which live in <code>~/.config/gcloud/application_default_credentials.json</code>.</p>
<p>In my case, those credentials didn’t have access to Firebase Auth for a
reason I did not try to understand.</p>
<p>However, what <em>did</em> have access to Firebase Auth is the application
default credentials as created by <a href="https://firebase.google.com/docs/cli#sign-in-test-cli"><code>firebase login</code></a>,
which live in <code>~/.config/firebase/*_application_default_credentials.json</code>.</p>
<p>Firebase’s <code>applicationDefault()</code>, despite being a method of the
Firebase SDK, <a href="https://github.com/firebase/firebase-admin-node/blob/ddcf965511e2f03853bad7658b5c61b85c306580/src/app/credential-internal.ts#L485">does <em>not</em> know</a>
about the Firebase application default credentials, and instead only
uses the Google Cloud credentials. 😅</p>
<p>However it supports reading the credentials file from the
<code>GOOGLE_APPLICATION_CREDENTIALS</code> environment variable, so we can run the
script like this:</p>
<pre><code class="hljs language-sh">GOOGLE_APPLICATION_CREDENTIALS=~/.config/firebase/*_application_default_credentials.json node script.js
</code></pre>
<div class="note">
<p><strong>Note:</strong> I left a wildcard <code>*</code> in the path above because Firebase
application default credentials contain your user and organization name.
It’ll work out of the box if you are only connected to a single Firebase
identity, but you’ll have to be more specific otherwise.</p>
</div>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Send Cloudflare Workers logs to Google Cloud Logging using Logpush</title>
    <link href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html" />
    <id>https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html</id>
    <updated>2024-05-05T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Cloudflare Workers are great, until they become a key part of your
production system and you realize you don’t have any logs. 😅</p>
<p>Something didn’t work the way it should? Woops, sorry, can’t do much
about that, I have no trace of what happened. 🤷</p>
<p>Not ideal.</p>
<div class="note">
<p><strong>Note:</strong> sure there’s the option to tail logs from the dashboard and
the CLI, but it turns out most of the logs I need don’t get logged while
I’m watching. 👀</p>
</div>
<p>For a while, the alternative was to replace <code>console.log</code> statements by
<code>fetch</code> requests to something that will actually persist logs. Fine, but
still not ideal.</p>
<p>Thankfully they introduced <a href="https://blog.cloudflare.com/logpush-for-workers/">Logpush for Workers</a>
back in 2022, which finally gave us a way to forward worker logs to a
number of <a href="https://developers.cloudflare.com/logs/get-started/enable-destinations/">destinations</a>,
including Amazon S3, Google Cloud Storage, Datadog, Elasticsearch,
BigQuery and more.</p>
<p>But none of those options was Google Cloud Logging. And I like to
centralize my logs in Google Cloud Logging. Bummer.</p>
<h2 id="leveraging-the-http-destination" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#leveraging-the-http-destination"><span>Leveraging the HTTP destination</span></a></h2>
<p>One of those options though is an arbitrary <a href="https://developers.cloudflare.com/logs/get-started/enable-destinations/http/">HTTP destination</a>.</p>
<p>With that, I should be able to integrate any log backend I want.</p>
<p>What if I made a Cloudflare Worker to handle the logs of my other
workers? That log drain worker probably shouldn’t drain logs to itself
to avoid an infinite recursion, but I could fallback in one of the other
integrations just for this one.</p>
<h2 id="configuring-a-logpush-handler" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#configuring-a-logpush-handler"><span>Configuring a Logpush handler</span></a></h2>
<p>In order to use Cloudflare Logpush, <a href="https://developers.cloudflare.com/logs/about/">you need to be under the Cloudflare Enterprise plan</a>.
However there’s an exception for the Cloudflare Workers logs! Then all
you need is the <a href="https://developers.cloudflare.com/workers/platform/pricing/">Workers Paid</a>
plan.</p>
<p>You can configure a log handler from your dashboard, in <strong>Analytics &amp;
Logs &gt; Logs &gt; Add Logpush job</strong>. Select <strong>Workers trace events</strong> as a
dataset, select the fields you care about (more on that later), and
configure your HTTP endpoint.</p>
<p>This UI looks like a recent addition! When I originally worked on this,
Logpush was only configurable <a href="https://developers.cloudflare.com/logs/get-started/enable-destinations/http/#manage-via-api">by using the Cloudflare HTTP API</a>.</p>
<p>For the record, and because it gives you more control over the Logpush
settings, here’s how you would do this with the API.</p>
<p>First, you need an API token, which you can create from <strong>My Profile &gt;
API Tokens</strong>.</p>
<p>While the API docs often reference the usage of API keys with
<code>X-Auth-Email</code> and <code>X-Auth-Key</code> headers, those API keys have complete
permissions over your account, and I would recommend against using them
if you have a better alternative.</p>
<p>The better alternative: API <em>tokens</em>, which lets you scope permissions.
In our case, we want to create a custom token with permissions of
<code>Zone.Logs.Edit</code>. That token can then be used in a <code>Authorization: Bearer</code> header.</p>
<p>Here’s how you would list existing Logpush jobs:</p>
<pre><code class="hljs language-sh">curl \
    -H <span class="hljs-string">&quot;Authorization: Bearer <span class="hljs-variable">$TOKEN</span>&quot;</span> \
    <span class="hljs-string">&#x27;https://api.cloudflare.com/client/v4/accounts/my-account-id/logpush/jobs&#x27;</span>
</code></pre>
<p>Where <code>my-account-id</code> is your account ID, that you can find for example
in the <strong>Workers &amp; Pages &gt; Overview</strong> page on the right.</p>
<p>To create a job:</p>
<pre><code class="hljs language-sh">curl \
    -H <span class="hljs-string">&quot;Authorization: Bearer <span class="hljs-variable">$TOKEN</span>&quot;</span> \
    -H <span class="hljs-string">&#x27;Content-Type: application/json&#x27;</span> \
    <span class="hljs-string">&#x27;https://api.cloudflare.com/client/v4/accounts/my-account-id/logpush/jobs&#x27;</span> \
    --data <span class="hljs-string">&#x27;{
  &quot;name&quot;: &quot;test&quot;,
  &quot;output_options&quot;: {
    &quot;field_names&quot;: [&quot;DispatchNamespace&quot;, &quot;Entrypoint&quot;, &quot;Event&quot;, &quot;EventTimestampMs&quot;, &quot;EventType&quot;, &quot;Exceptions&quot;, &quot;Logs&quot;, &quot;Outcome&quot;, &quot;ScriptName&quot;, &quot;ScriptTags&quot;, &quot;ScriptVersion&quot;],
    &quot;timestamp_format&quot;: &quot;rfc3339&quot;
  },
  &quot;destination_conf&quot;: &quot;https://my.worker.workers.dev&quot;,
  &quot;dataset&quot;: &quot;workers_trace_events&quot;,
  &quot;enabled&quot;: true
}&#x27;</span>
</code></pre>
<p>Where the API shines compared to the UI, is that you can configure a
number of <a href="https://developers.cloudflare.com/api/operations/post-accounts-account_identifier-logpush-jobs#request-body">extra options</a>
like <code>max_upload_bytes</code>, <code>max_upload_interval_seconds</code> and
<code>max_upload_records</code>, to make sure Logpush makes requests within
acceptable limits for your endpoint.</p>
<p>In our case, the Logpush handler is also a Cloudflare worker so the max
body size will be between 100 MB and 500 MB <a href="https://developers.cloudflare.com/workers/platform/limits/#request-limits">depending on your plan</a>.
But also, Cloudflare workers have a <a href="https://developers.cloudflare.com/workers/platform/limits/">memory limit</a>
of 128 MB so that’s something to take into account as well. Oh and keep
in mind <a href="https://community.cloudflare.com/t/workers-memory-limit/491329/2">this memory limit is per-isolate</a>
meaning that multiple requests could hit the same isolate. So adjust
accordingly, but I don’t have a silver bullet for this one. 🙃</p>
<div class="note">
<p><strong>Note:</strong> in my experience, setting <code>timestamp_format</code> to <code>rfc3339</code>
doesn’t do anything? I’m still only getting <code>TimestampMs</code> fields in
milliseconds (which interestingly is neither a <code>unix</code> (seconds) nor
<code>unixnano</code> (nanoseconds) timestamp, which are the other two possible
options).</p>
</div>
<p>In order to update a job, you’ll need the <code>id</code> that was returned by the
create request, or simply fetch it with the list request. It’s gonna be
like the create request but you append the log ID in the end, e.g.
<code>logpush/jobs/12345</code>, it’s a <code>PUT</code> request, and all fields are optional.</p>
<p>To delete a job, same but it’s a <code>DELETE</code> request with no body.</p>
<h2 id="the-logpush-http-destination-protocol" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#the-logpush-http-destination-protocol"><span>The Logpush HTTP destination protocol</span></a></h2>
<p>I didn’t find documentation about what the HTTP destination is supposed
to accept, so here’s what I figured out:</p>
<ul>
<li>It sends a <code>POST</code> request to the configured URL.</li>
<li>The body is gzipped.</li>
<li>The uncompressed body is a newline-delimited JSON of “events”, e.g.:</li>
</ul>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;DispatchNamespace&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Entrypoint&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Event&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;RayID&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;87ed87f80cf22d84&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Request&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;URL&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;https://test.workers.dev/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Method&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Response&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;Status&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">200</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;EventTimestampMs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">1714878560011</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;EventType&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;fetch&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Exceptions&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Logs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;Level&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;log&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-string">&quot;bar&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-string">&quot;foo&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;TimestampMs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">1714878560016</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Outcome&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;ok&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptName&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;test&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptTags&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptVersion&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;ID&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;1e7519b3-08e2-441d-ae10-7c8c6d3b7e17&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Tag&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;DispatchNamespace&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Entrypoint&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Event&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;RayID&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;87ed87fb49cb2d84&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Request&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;URL&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;https://test.workers.dev/&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Method&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;GET&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Response&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;Status&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">200</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;EventTimestampMs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">1714878560532</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;EventType&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;fetch&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Exceptions&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Logs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;Level&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;log&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-string">&quot;bar&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-string">&quot;foo&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;TimestampMs&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-number">1714878560532</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Outcome&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;ok&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptName&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;test&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptTags&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;ScriptVersion&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-punctuation">{</span><span class="hljs-attr">&quot;ID&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;1e7519b3-08e2-441d-ae10-7c8c6d3b7e17&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span><span class="hljs-attr">&quot;Tag&quot;</span><span class="hljs-punctuation">:</span><span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">}</span>
</code></pre>
<p>When you configure the HTTP destination, you get a chance to choose
which of those fields are included. You’ll get a different set of fields
depending on the kind of dataset you’re dealing with, but in the scope
of this article we’re focusing on worker logs.</p>
<p>For reference, here’s the list of supported <a href="https://developers.cloudflare.com/logs/reference/log-fields/zone/">zone-scoped datasets</a>
and <a href="https://developers.cloudflare.com/logs/reference/log-fields/account/">account-scoped datasets</a>.
Zone-scoped datasets like DNS logs are tied to a specific “zone”
(a specific domain), while account-scoped datasets like worker logs are
global to your account (workers don’t belong to a particular zone).</p>
<h2 id="writing-the-worker" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#writing-the-worker"><span>Writing the worker</span></a></h2>
<p>The worker will need to decompress the gzipped body, split it into lines
and send the individual logs to the Google Cloud Logging API.</p>
<p>Calling Google Cloud APIs from Cloudflare Workers is a bit of a
challenge because the Node.js SDK is not compatible with the workers
environment, so we need to reimplement the whole authentication process.
But it’s a problem <a href="https://www.codejam.info/2022/02/how-to-call-google-cloud-apis-from-cloudflare-workers.html">we’ve already solved in the past</a>
so it should be no big deal. 😎</p>
<p>First let’s start with the base of the worker, including decompressing
the body:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-keyword">async</span> <span class="hljs-title function_">fetch</span> (request) {
    <span class="hljs-keyword">if</span> (request.<span class="hljs-property">method</span> !== <span class="hljs-string">&#x27;POST&#x27;</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(<span class="hljs-string">&#x27;&#x27;</span>, { <span class="hljs-attr">status</span>: <span class="hljs-number">405</span> })
    }

    <span class="hljs-keyword">const</span> ds = <span class="hljs-keyword">new</span> <span class="hljs-title class_">DecompressionStream</span>(<span class="hljs-string">&#x27;gzip&#x27;</span>)
    <span class="hljs-keyword">const</span> stream = request.<span class="hljs-property">body</span>.<span class="hljs-title function_">pipeThrough</span>(ds)
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>(stream).<span class="hljs-title function_">text</span>()
    <span class="hljs-keyword">const</span> logs = body.<span class="hljs-title function_">split</span>(<span class="hljs-string">&#x27;\n&#x27;</span>)

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> json <span class="hljs-keyword">of</span> logs) {
      <span class="hljs-keyword">if</span> (json.<span class="hljs-title function_">trim</span>() === <span class="hljs-string">&#x27;&#x27;</span>) {
        <span class="hljs-keyword">continue</span>
      }

      <span class="hljs-keyword">const</span> log = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(json)

      <span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(log)
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Response</span>()
  }
}
</code></pre>
<p>To do that, we use the native
<a href="https://developer.mozilla.org/en-US/docs/Web/API/DecompressionStream"><code>DecompressionStream</code></a>,
that we can then <a href="https://stackoverflow.com/a/72718732/4324668">convert to text</a>
with <code>await new Response(stream).text()</code>.</p>
<h2 id="calling-the-google-cloud-logging-api" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#calling-the-google-cloud-logging-api"><span>Calling the Google Cloud Logging API</span></a></h2>
<p>Now let’s see how we can call the Google Cloud Logging API. Again, we
can’t use the Google Cloud Node.js SDK, so we need to call the REST API
manually. Everything about authentication is explained in
<a href="https://www.codejam.info/2022/02/how-to-call-google-cloud-apis-from-cloudflare-workers.html">this post</a>
so I won’t cover that again. Read that article to understand how to deal
with Google Cloud API authentication from a Cloudflare worker!</p>
<p>When it comes to Google Cloud Logging specifically, you’ll need an <code>aud</code>
of <code>https://logging.googleapis.com/</code> in your JWT. The rest of this post
will assume you generated a <code>token</code> variable thanks to the
aforementioned article.</p>
<p>In order to write logs, we need to call the <a href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/entries/write"><code>entries:write</code></a>
endpoint.</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(
  <span class="hljs-string">`https://logging.googleapis.com/v2/entries:write`</span>,
  {
    <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>,
    <span class="hljs-attr">headers</span>: {
      <span class="hljs-string">&#x27;Content-Type&#x27;</span>: <span class="hljs-string">&#x27;application/json&#x27;</span>,
      <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${token}</span>`</span>
    },
    <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>({
      <span class="hljs-attr">entries</span>: [
        {
          <span class="hljs-attr">logName</span>: <span class="hljs-string">`projects/my-project-id/logs/my-log-id`</span>,
          <span class="hljs-attr">resource</span>: {
            <span class="hljs-attr">type</span>: <span class="hljs-string">&#x27;generic_node&#x27;</span>,
            <span class="hljs-attr">labels</span>: {
              <span class="hljs-comment">// project_id: &#x27;...&#x27;,</span>
              <span class="hljs-comment">// location: &#x27;...&#x27;,</span>
              <span class="hljs-comment">// namespace: &#x27;...&#x27;,</span>
              <span class="hljs-comment">// node_id: &#x27;...&#x27;</span>
            }
          },
          <span class="hljs-comment">// severity: &#x27;DEFAULT&#x27;,</span>
          <span class="hljs-attr">timestamp</span>: <span class="hljs-string">&#x27;2024-05-05T17:38:47.512Z&#x27;</span>,
          <span class="hljs-attr">jsonPayload</span>: {
            <span class="hljs-attr">foo</span>: <span class="hljs-string">&#x27;bar
          }
        }
      ]
    })
  }
)
</span></code></pre>
<p>Replace <code>my-project-id</code> by your project ID. <code>my-log-id</code> can be anything.</p>
<p>Here I chose to use a <code>generic_node</code> resource type, but there’s
<a href="https://cloud.google.com/logging/docs/api/v2/resource-list#resource-types">quite a lot of other choices</a>
so feel free to use what makes the most sense to you.</p>
<p>The resource type that you chose will have a number of associated labels
that you can feed. In this example I included the <code>generic_node</code> labels.
<code>project_id</code> doesn’t really need to be set because it will be
automatically populated from the project ID in your <code>logName</code>. The other
ones are also optional. Put what makes the most sense for your data!</p>
<p>There’s a number of other fields you can set on each <a href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry">log entry</a>
in the <code>entries</code> array, but I kept it simple for this example.</p>
<p>You can for example tune the
<a href="https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity"><code>severity</code></a>,
e.g. to distinguish <code>WARNING</code> and <code>ERROR</code> logs appropriately.</p>
<h2 id="formatting-logpush-logs-to-google-cloud-logging-entries" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#formatting-logpush-logs-to-google-cloud-logging-entries"><span>Formatting Logpush logs to Google Cloud Logging entries</span></a></h2>
<p>Let’s look in a bit more details at a worker log received from Logpush:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;DispatchNamespace&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Entrypoint&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Event&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;RayID&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;87ed87f80cf22d84&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;Request&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;URL&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;https://test.workers.dev/&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;Method&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;GET&quot;</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;Response&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;Status&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">200</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;EventTimestampMs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1714878560011</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;EventType&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;fetch&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Exceptions&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Logs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;Level&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;log&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
        <span class="hljs-string">&quot;bar&quot;</span><span class="hljs-punctuation">,</span>
        <span class="hljs-string">&quot;foo&quot;</span>
      <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;TimestampMs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1714878560016</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;Outcome&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;ok&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;ScriptName&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;test&quot;</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;ScriptTags&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;ScriptVersion&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;ID&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;1e7519b3-08e2-441d-ae10-7c8c6d3b7e17&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;Message&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">&quot;Tag&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;&quot;</span>
  <span class="hljs-punctuation">}</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>And here’s the associated <a href="https://developers.cloudflare.com/logs/reference/log-fields/account/workers_trace_events/">docs</a>.</p>
<p>As we can see, we get one entry per “event” which in this case, is a
whole HTTP request completing.</p>
<p>Then for this particular HTTP request, we’ve got an array of <code>Logs</code> that
the worker outputted during its runtime.</p>
<div class="note">
<p><strong>Note:</strong> interestingly, the above log <code>[&quot;bar&quot;, &quot;foo&quot;]</code> was generated
by:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">console</span>.<span class="hljs-title function_">log</span>(<span class="hljs-string">&#x27;foo&#x27;</span>, <span class="hljs-string">&#x27;bar&#x27;</span>)
</code></pre>
<p>So it looks like the <code>Message</code> array is the reverse of the arguments
order that was passed to <code>console.log</code>.</p>
<p>Weird, but OK.</p>
</div>
<p>From there, it’s up to you how you translate that to Google Cloud
Logging entries. You could:</p>
<ol>
<li>Use the whole “event” as a single log entry and dig in the <code>Logs</code>
property to see the actual logs. Then you could set the log severity
based on the response status code, e.g. <code>ERROR</code> if the status is
<code>&gt;=400</code>.</li>
<li>Store the HTTP request “event” without logs in a separate entry, then
map the <code>Logs</code> array to individual log entries. Then you could map
the <code>Level</code> property to a log severity and have more granularity that
way.</li>
</ol>
<p>For this post, I’ll take the lazy approach and just shove the whole
thing in the <code>jsonPayload</code>. 😄</p>
<p>Building off the <a href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#writing-the-worker">worker base from earlier</a>:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> entries = []

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> json <span class="hljs-keyword">of</span> logs) {
  <span class="hljs-keyword">if</span> (json.<span class="hljs-title function_">trim</span>() === <span class="hljs-string">&#x27;&#x27;</span>) {
    <span class="hljs-keyword">continue</span>
  }

  <span class="hljs-keyword">const</span> log = <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">parse</span>(json)

  entries.<span class="hljs-title function_">push</span>({
    <span class="hljs-attr">logName</span>: <span class="hljs-string">`projects/my-project-id/logs/my-log-id`</span>,
    <span class="hljs-attr">resource</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-string">&#x27;generic_node&#x27;</span>,
      <span class="hljs-attr">labels</span>: {
        <span class="hljs-attr">namespace</span>: log.<span class="hljs-property">ScriptName</span>
      }
    },
    <span class="hljs-attr">severity</span>: log.<span class="hljs-property">Event</span>.<span class="hljs-property">Response</span>.<span class="hljs-property">Status</span> &gt;= <span class="hljs-number">400</span> ? <span class="hljs-string">&#x27;ERROR&#x27;</span> : <span class="hljs-string">&#x27;DEFAULT&#x27;</span>,
    <span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>(log.<span class="hljs-property">EventTimestampMs</span>).<span class="hljs-title function_">toISOString</span>(),
    <span class="hljs-attr">jsonPayload</span>: log
  })
}
</code></pre>
<p>Then as we saw before, we can push those entries to Google Cloud
Logging:</p>
<pre><code class="hljs language-js"><span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> <span class="hljs-title function_">fetch</span>(
  <span class="hljs-string">`https://logging.googleapis.com/v2/entries:write`</span>,
  {
    <span class="hljs-attr">method</span>: <span class="hljs-string">&#x27;POST&#x27;</span>,
    <span class="hljs-attr">headers</span>: {
      <span class="hljs-string">&#x27;Content-Type&#x27;</span>: <span class="hljs-string">&#x27;application/json&#x27;</span>,
      <span class="hljs-title class_">Authorization</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${token}</span>`</span>
    },
    <span class="hljs-attr">body</span>: <span class="hljs-title class_">JSON</span>.<span class="hljs-title function_">stringify</span>({
      entries
    })
  }
)
</code></pre>
<h2 id="make-your-workers-use-logpush" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#make-your-workers-use-logpush"><span>Make your workers use Logpush!</span></a></h2>
<p>The final step is to enable Logpush on your workers. By default, even if
you have Logpush destinations enabled, they won’t be used unless
explicitly enabled at the worker level as well.</p>
<p>You can do that from the UI in your worker page, in <strong>Logs &gt; Event logs
Workers Logpush</strong>. If you use the Wrangler CLI, make sure to also set
<code>logpush = true</code> in your <code>wrangler.toml</code>!</p>
<h2 id="final-thoughts" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/cloudflare-workers-logs-gcp-logging-logpush.html#final-thoughts"><span>Final thoughts</span></a></h2>
<p>Getting your Cloudflare Workers logs onto Google Cloud Logging is not
easy, and using a Cloudflare worker for the integration layer makes it
even harder, but it’s also kinda cool if you ask me. 😏</p>
<p>You should now have everything you need to implement that, from the
details of using the Cloudflare API to create Logpush jobs and tune it
in a way you can’t do from the UI, implementing a HTTP Logpush
destination with gzip support, parsing the Logpush payload, all the way
to translating it for Google Cloud Logging and push it using the raw
HTTP API in an environment where the official SDK is not supported.</p>
<p>I hope you learnt a thing or two thanks to this post, and that your logs
are being happily ingested now! 🫶</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Join the discussion on <a href="https://x.com/valeriangalliat/status/1787213611267674227">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Next.js: make Firebase Auth signInWithRedirect work with Safari</title>
    <link href="https://www.codejam.info/2024/05/nextjs-firebase-auth-safari.html" />
    <id>https://www.codejam.info/2024/05/nextjs-firebase-auth-safari.html</id>
    <updated>2024-05-04T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Had that
<a href="https://github.com/firebase/firebase-js-sdk/issues/6716">issue</a>
back in 2022 and it’s now a pretty
<a href="https://firebase.google.com/docs/auth/web/redirect-best-practices">well-understood</a>
problem, but better write about it later than never. 😂</p>
<p>Essentially, in Safari 16.1+ (and now Firefox 109+), there are more
aggressive restrictions on third-party cookies that mess with the way
Firebase Auth <code>signInWithRedirect</code> is implemented.</p>
<p>By default, your app could be running on <code>https://myapp.com</code> but
<code>signInWithRedirect</code> would redirect to
<code>https://myapp.firebaseapp.com/__/auth</code> and then back to your app in
order to handle the auth. The message passing with third-party cookies
between those two hosts is no longer possible in Safari, Firefox, and
soon Chrome.</p>
<p>Firebase docs now document <a href="https://firebase.google.com/docs/auth/web/redirect-best-practices">5 options</a>
to solve that.</p>
<ol>
<li>If you host your app on Firebase, make sure your Firebase config
<code>authDomain</code> point to your custom domain and not
<code>myapp.firebaseapp.com</code>. Because Firebase hosts your app, it will
automatically handle the special <code>__/auth</code> path.</li>
<li>Use <code>signInWithPopup</code> which doesn’t depend on third-party cookies.</li>
<li>If your frontend is not hosted on Firebase, proxy requests from
<code>https://myapp.com/__auth/*</code> to <code>https://myapp.firebaseapp.com/__/auth/*</code>
so there’s no cross-domain concerns.</li>
<li>Download the relevant files from
<code>https://myapp.firebaseapp.com/__/auth/*</code> and “self-host” them on
your app.</li>
<li>Handle provider auth by yourself.</li>
</ol>
<p>In my case, I’m not hosting the website on Firebase, and I don’t want to
use <code>signInWithPupup</code>, so the proxy looks like a solid option.</p>
<p>In a Next.js app, it’s as easy as adding the following to
<code>next.config.js</code>:</p>
<pre><code class="hljs language-js"><span class="hljs-variable language_">module</span>.<span class="hljs-property">exports</span> = {
  <span class="hljs-keyword">async</span> <span class="hljs-title function_">rewrites</span> () {
    <span class="hljs-keyword">return</span> {
      <span class="hljs-attr">beforeFiles</span>: [
        {
          <span class="hljs-attr">source</span>: <span class="hljs-string">&#x27;/__/auth/:path*&#x27;</span>,
          <span class="hljs-attr">destination</span>: <span class="hljs-string">`https://myapp.firebaseapp.com/__/auth/:path*`</span>
        }
      ]
    }
  }
}
</code></pre>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>PostgreSQL: swap tables with dependent views</title>
    <link href="https://www.codejam.info/2024/05/postgresql-swap-tables-dependent-views.html" />
    <id>https://www.codejam.info/2024/05/postgresql-swap-tables-dependent-views.html</id>
    <updated>2024-05-04T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>Sometimes, you need to do some maintenance on a table, and doing a
<em>table swap</em> is a good tool to avoid downtime (e.g. if the maintenance
would lock aggressively and run for a long time). The idea is as
follows:</p>
<ol>
<li>Clone the source table.</li>
<li>Perform the maintenance.</li>
<li>Make sure they’re in sync if needs be.</li>
<li>When the maintenance is over, in a transaction, drop the source
table, and rename the clone to the original name.</li>
</ol>
<p>It can get a bit more complicated than that if you have foreign keys,
but I won’t cover that in this article.</p>
<p>However, another way it gets more complicated is when you have <em>views</em>
that depend on the table you want to swap:</p>
<pre><code class="hljs language-sql"><span class="hljs-keyword">BEGIN</span>;
<span class="hljs-keyword">DROP</span> <span class="hljs-keyword">TABLE</span> example;
<span class="hljs-keyword">ALTER TABLE</span> example_swap RENAME <span class="hljs-keyword">TO</span> example;
</code></pre>
<pre><code class="hljs">ERROR: cannot drop table example because other objects depend on it
DETAIL: view example_view depends on table example
</code></pre>
<p>In this case, we need to update <code>example_view</code> (and all other views that
depend on <code>example</code>) to reference the <code>example_swap</code> table before we
perform the actual swap.</p>
<p>If this is a one-off swap, fine, but if you’re doing the swap as part of
some automated maintenance task, that won’t do it.</p>
<h2 id="automatically-swapping-dependent-views" tabindex="-1"><a class="header-anchor" href="https://www.codejam.info/2024/05/postgresql-swap-tables-dependent-views.html#automatically-swapping-dependent-views"><span>Automatically swapping dependent views</span></a></h2>
<p>In my case, the dependent views don’t change very often (if at all), so
I went with a static list of the views that depend on the table I need
to swap.</p>
<p>Then I use the following script to swap the views:</p>
<pre><code class="hljs language-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> REPLACE <span class="hljs-keyword">FUNCTION</span> pg_temp.replace_view_table(view_schema text, view_name text, old_table text, new_table text) <span class="hljs-keyword">RETURNS</span> void <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    view_definition text;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">SELECT</span> definition <span class="hljs-keyword">INTO</span> view_definition
    <span class="hljs-keyword">FROM</span> pg_views
    <span class="hljs-keyword">WHERE</span> schemaname <span class="hljs-operator">=</span> view_schema
    <span class="hljs-keyword">AND</span> viewname <span class="hljs-operator">=</span> view_name;

    view_definition :<span class="hljs-operator">=</span> REPLACE(view_definition, old_table, new_table);

    <span class="hljs-keyword">EXECUTE</span> <span class="hljs-string">&#x27;CREATE OR REPLACE VIEW &#x27;</span> <span class="hljs-operator">||</span> view_schema <span class="hljs-operator">||</span> <span class="hljs-string">&#x27;.&#x27;</span> <span class="hljs-operator">||</span> view_name <span class="hljs-operator">||</span> <span class="hljs-string">&#x27; AS &#x27;</span> <span class="hljs-operator">||</span> view_definition;
<span class="hljs-keyword">END</span>;
$$ <span class="hljs-keyword">LANGUAGE</span> plpgsql;
</code></pre>
<p>This function will redefine the view to point to the new swap table. It
does a basic search and replace in the SQL definition of the view, so
you need to make sure the table name doesn’t conflict with anything else
in there.</p>
<div class="note">
<p><strong>Note:</strong> I’m using <code>pg_temp</code> so that the function is local to the
current database connection. I don’t want to leave it around permanently
in that case.</p>
</div>
<p>You can now perform the swap as follows:</p>
<pre><code class="hljs language-sql"><span class="hljs-keyword">BEGIN</span>;
<span class="hljs-keyword">SELECT</span> pg_temp.replace_view_table(<span class="hljs-string">&#x27;public&#x27;</span>, <span class="hljs-string">&#x27;example_view&#x27;</span>, <span class="hljs-string">&#x27;example&#x27;</span>, <span class="hljs-string">&#x27;example_swap&#x27;</span>);
<span class="hljs-keyword">DROP</span> <span class="hljs-keyword">TABLE</span> example;
<span class="hljs-keyword">ALTER TABLE</span> example_swap RENAME <span class="hljs-keyword">TO</span> example;
<span class="hljs-keyword">COMMIT</span>;
</code></pre>
<p>The renaming of the table will automatically propagate to the dependent
views, they won’t keep referencing the now gone <code>example_swap</code> table,
they’ll properly point to <code>example</code>! 🥳</p>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
  <entry>
    <title>Vercel monorepo: properly cache Yarn installs</title>
    <link href="https://www.codejam.info/2024/05/vercel-monorepo-cache-yarn-installs.html" />
    <id>https://www.codejam.info/2024/05/vercel-monorepo-cache-yarn-installs.html</id>
    <updated>2024-05-04T07:00:00.000Z</updated>
    <content type="html"><![CDATA[<p>So you have a Vercel app that’s part of a monorepo. You may have noticed
that by default it installs the whole monorepo dependencies, and you may
have already <a href="https://www.codejam.info/2024/05/vercel-monorepo-single-project-dependencies-yarn.html">addressed that</a>!</p>
<p>But either way, you have another problem: Yarn downloads all your
dependencies on every single build. That’s pretty time consuming. It
doesn’t seem that dependencies are getting cached at all.</p>
<p>Vercel <a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-2036114">recommends</a>
setting a <code>ENABLE_ROOT_PATH_BUILD_CACHE=1</code> environment variable to
<a href="https://vercel.com/changelog/faster-build-times-for-monorepos">make build times faster in monorepos</a>.</p>
<p>It sounds great, but in my experience it didn’t do anything, and I’m not
<a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-2745510">the</a>
<a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-5105483">only</a>
<a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-7077684">one</a>.</p>
<p>It <a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-5166537"><em>seems</em></a>
that regardless of <code>ENABLE_ROOT_PATH_BUILD_CACHE</code>, Vercel doesn’t
cache Yarn’s cache folder <code>.yarn/cache</code>, and Yarn 3 and greater will
download everything again if this directory is not present, regardless
of the state of <code>node_modules</code>.</p>
<p>So the key is to force Yarn’s cache folder to be inside a directory that
Vercel actually caches.</p>
<p>I <a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-4295643">tried</a>
setting it inside the root <code>node_modules</code> by doing
<code>YARN_CACHE_FOLDER=../../node_modules/.yarn-cache yarn workspaces focus</code>,
which worked at first, but quickly encountered some
<a href="https://github.com/orgs/vercel/discussions/222#discussioncomment-5165657">issues</a>
when the cache was reused across different build states.</p>
<p>Luckily, thanks to <a href="https://github.com/vercel/turbo/issues/785#issuecomment-1060054306">this comment</a>
on a totally unrelated issue, I discovered I could put the cache in
<code>.next/cache</code> instead, and I never had issues since then!</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;installCommand&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;YARN_CACHE_FOLDER=.next/cache/yarn-cache yarn workspaces focus&quot;</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<section class="post-footer">
  <h3>Want to leave a comment?</h3>
  <p>
    Start a conversation on <a href="https://x.com/valeriangalliat">X</a> or send me an <a href="mailto:val@codejam.info">email</a>! 💌<br>
    This post helped you? <a href="https://ko-fi.com/funkyval">Buy me a coffee</a>! 🍻
  </p>
</section>
]]></content>
  </entry>
</feed>
