<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Christoph Grothaus</title>
    <description>The latest articles on DEV Community by Christoph Grothaus (@cgrothaus).</description>
    <link>https://dev.to/cgrothaus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F725906%2F24ad398f-5277-4071-82e2-9d06eba93688.jpeg</url>
      <title>DEV Community: Christoph Grothaus</title>
      <link>https://dev.to/cgrothaus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cgrothaus"/>
    <language>en</language>
    <item>
      <title>Missing Rails Console? Building a Database REPL for NestJS + Prisma</title>
      <dc:creator>Christoph Grothaus</dc:creator>
      <pubDate>Thu, 11 Sep 2025 11:30:04 +0000</pubDate>
      <link>https://dev.to/cgrothaus/missing-rails-console-building-a-database-repl-for-nestjs-prisma-4p0i</link>
      <guid>https://dev.to/cgrothaus/missing-rails-console-building-a-database-repl-for-nestjs-prisma-4p0i</guid>
      <description>&lt;p&gt;&lt;em&gt;TL;DR: Coming from Rails/Phoenix, I missed having an interactive database console. So I built one for my NestJS + Prisma stack. Here's how and why.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: No Interactive Database Console 😢
&lt;/h2&gt;

&lt;p&gt;If you've ever worked with &lt;strong&gt;Ruby on Rails&lt;/strong&gt; or &lt;strong&gt;Elixir Phoenix&lt;/strong&gt;, you know the joy of having an interactive console at your fingertips:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rails&lt;/span&gt;
rails console
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; User.where&lt;span class="o"&gt;(&lt;/span&gt;active: &lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;.count
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; 1337

&lt;span class="c"&gt;# Phoenix&lt;/span&gt;
iex &lt;span class="nt"&gt;-S&lt;/span&gt; mix
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; MyApp.Repo.all&lt;span class="o"&gt;(&lt;/span&gt;User&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These REPLs are &lt;strong&gt;game-changers&lt;/strong&gt; for development. Need to quickly test a query? Explore your data model? Debug a complex association? Just fire up the console and start experimenting.&lt;/p&gt;

&lt;p&gt;But when I switched to &lt;strong&gt;NestJS + Prisma&lt;/strong&gt;... 🤷‍♂️&lt;/p&gt;

&lt;p&gt;Sure, there's &lt;code&gt;prisma studio&lt;/code&gt; for a GUI, but sometimes you just want to &lt;strong&gt;code your way through the data&lt;/strong&gt;. You want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Quickly prototype complex queries&lt;/li&gt;
&lt;li&gt;Test edge cases without writing full test files&lt;/li&gt;
&lt;li&gt;Explore data relationships interactively&lt;/li&gt;
&lt;li&gt;Debug production data issues with real queries&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: DIY Database Console 🛠️
&lt;/h2&gt;

&lt;p&gt;So I built my own (of course with a lot AI assistance by Claude Sonnet)! Meet the &lt;strong&gt;Prisma DB Console&lt;/strong&gt; - a Rails-like interactive REPL for Prisma:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="cp"&gt;#!/usr/bin/env ts-node
&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PrismaClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@prisma/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;repl&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;repl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;util&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;util&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Initialize Prisma with query logging&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PrismaClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;log&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Log SQL queries with params and timing&lt;/span&gt;
&lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;---&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key Features That Make It Awesome ✨
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. &lt;strong&gt;Prisma Client Pre-loaded&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;No more import boilerplate - just start querying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. &lt;strong&gt;SQL Query Visibility&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;See exactly what SQL gets generated, with parameters and execution time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
SELECT "User"."id", "User"."email" FROM "User"
[]
12ms
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. &lt;strong&gt;Helpful Examples on Startup&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Because who remembers all the Prisma syntax?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🚀 Prisma DB Console
📊 Prisma Client loaded as "prisma"
💡 Examples:
   await prisma.user.findMany()
   await prisma.tenant.count()
   await prisma.$queryRaw`SELECT COUNT(*) FROM "user"`
   await prisma.organization.findFirst({ include: { tenants: true } })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;strong&gt;Clean Exit Handling&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Properly disconnects from the database when you exit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;cleanupAndExit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;👋 Disconnecting from database...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;prisma&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$disconnect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Usage: Simple as &lt;code&gt;npm run db:console&lt;/code&gt; 🚀
&lt;/h2&gt;

&lt;p&gt;Added it as an npm script for easy access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"db:console"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ts-node bin/db-console.ts"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I can just run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run db:console
&lt;span class="c"&gt;# or&lt;/span&gt;
pnpm db:console
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And I'm off to the races! Perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Data exploration&lt;/strong&gt;: &lt;code&gt;await prisma.user.findMany({ include: { profile: true } })&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quick counts&lt;/strong&gt;: &lt;code&gt;await prisma.order.count({ where: { status: 'PENDING' } })&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complex queries&lt;/strong&gt;: &lt;code&gt;await prisma.$queryRaw\&lt;/code&gt;SELECT * FROM ...``&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testing relationships&lt;/strong&gt;: Verify those Prisma relations work as expected&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Impact 📈
&lt;/h2&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Writing a throwaway script&lt;/li&gt;
&lt;li&gt;Adding console.logs&lt;/li&gt;
&lt;li&gt;Running the script&lt;/li&gt;
&lt;li&gt;Deleting the script&lt;/li&gt;
&lt;li&gt;Repeat...&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I just:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open console&lt;/li&gt;
&lt;li&gt;Experiment&lt;/li&gt;
&lt;li&gt;Done!&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Want to Build Your Own? 🔨
&lt;/h2&gt;

&lt;p&gt;The full implementation is about 70 lines of TypeScript. Key ingredients:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node's built-in &lt;code&gt;repl&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;Your Prisma client&lt;/li&gt;
&lt;li&gt;Event-based query logging&lt;/li&gt;
&lt;li&gt;Proper cleanup handlers&lt;/li&gt;
&lt;li&gt;Some helpful utilities in the REPL context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The beauty is in its simplicity - no external dependencies beyond what you already have in a NestJS + Prisma project. The full code of my &lt;code&gt;db-console.ts&lt;/code&gt; is available on &lt;a href="https://gist.github.com/cgrothaus/e41aa8a28f0c2309bc72d56634988be8" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt; if you want to copy and adapt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing Thoughts 💭
&lt;/h2&gt;

&lt;p&gt;Sometimes the &lt;strong&gt;best tools are the ones you build yourself&lt;/strong&gt;. They fit your exact workflow, solve your specific pain points, and give you that Rails-like developer experience you've been missing.&lt;/p&gt;

&lt;p&gt;If you're working with NestJS + Prisma and find yourself constantly writing throwaway scripts to test database queries, consider building your own console. Your future self will thank you!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What is your experience in the NestJS + Prisma ecosystem? Did I miss out on an existing solution? Just drop a comment below!&lt;/em&gt;&lt;/p&gt;




</description>
      <category>nestjs</category>
      <category>prisma</category>
      <category>productivity</category>
      <category>dx</category>
    </item>
    <item>
      <title>gRPC client doesn't like Let's Encrypt cross-signed root certs</title>
      <dc:creator>Christoph Grothaus</dc:creator>
      <pubDate>Thu, 14 Oct 2021 21:24:58 +0000</pubDate>
      <link>https://dev.to/cgrothaus/grpc-client-doesnt-like-lets-encrypt-cross-signed-root-certs-93g</link>
      <guid>https://dev.to/cgrothaus/grpc-client-doesnt-like-lets-encrypt-cross-signed-root-certs-93g</guid>
      <description>&lt;p&gt;This is a story about gRPC client and a Let's Encrypt certificate chain that is cross-signed by two root certificates.&lt;/p&gt;

&lt;h1&gt;
  
  
  Our context
&lt;/h1&gt;

&lt;p&gt;In one of our apps, we have a gRPC client that connects to a gRPC server of a 3rd party service that we use. The app is written in Ruby, thus we use the Ruby flavour of gRPC, which uses the &lt;a href="https://github.com/grpc/grpc"&gt;gRPC core C library&lt;/a&gt; under the hood.&lt;/p&gt;

&lt;p&gt;As many do, said 3rd party service uses SSL connections secured by Let's Encrypt (LE) certs. There is a certificate chain that goes from those per-service LE certs to a LE certificate authority (CA) cert, which again is signed by widely known root certs. In this case it even was &lt;em&gt;cross-signed&lt;/em&gt; by two certs: &lt;em&gt;DST Root CA X3&lt;/em&gt; and &lt;em&gt;ISRG Root X1&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The first one had been used by LE longer ago, to &lt;em&gt;"get off the ground"&lt;/em&gt;, as they write, because it was widely trusted. In recent times LE preferred the second, newer one, but still supported the older one via cross-sign.&lt;/p&gt;

&lt;p&gt;On Sep. 30th 2021, that older DST Root CA X3 cert expired (see this &lt;a href="https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/"&gt;Let's Encrypt blogpost&lt;/a&gt; explaining the background). While that shouldn't have been a problem, because the ISRG Root X1 was also part of the chain, this caused us troubles.&lt;/p&gt;

&lt;h1&gt;
  
  
  The problem
&lt;/h1&gt;

&lt;p&gt;Last week – roughly one week after Sep. 30th – we restarted our app for the first time since weeks, through a deployment by CD pipeline. After restart, the gRPC client could not establish SSL connections to its gRPC server anymore. We got errors saying the server cert was invalid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E1006 13:48:18.326843613      93 ssl_transport_security.cc:1446] Handshake failed with fatal error SSL_ERROR_SSL: error:1000007d:SSL routines:OPENSSL_internal:CERTIFICATE_VERIFY_FAILED.
GRPC::Unavailable: 14:failed to connect to all addresses.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After some digging, we found out there is a &lt;a href="https://github.com/grpc/grpc/issues/27532"&gt;bug in the gRPC core C library&lt;/a&gt;. It didn't get it that there are two root certs in the chain, out of which one was still valid. It stuck with the invalid DST Root CA X3 cert, and thus failed cert verification.&lt;/p&gt;

&lt;p&gt;Reportedly, only the core library – and by that all languages building on it, like Ruby or Python – was affected, but not the Java or the .Net version.&lt;/p&gt;

&lt;h1&gt;
  
  
  The solution
&lt;/h1&gt;

&lt;p&gt;We could not wait for the bugfix. To get our app back running, we needed to persuade the gRPC client that the server cert indeed was valid. Therefore we removed the now-invalid DST Root CA X3 root cert from our Docker image, rebuilt the collection of CA certs, and forced gRPC to use that. That did the trick.&lt;/p&gt;

&lt;p&gt;This is what we added to the Dockerfile (on a Debian base image):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# remove the now-invalid root cert&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; /usr/share/ca-certificates/mozilla/DST_Root_CA_X3.crt
&lt;span class="c"&gt;# rebuild collection of root certs&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;update-ca-certificates
&lt;span class="c"&gt;# force gRPC to use that collection&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; GRPC_DEFAULT_SSL_ROOTS_FILE_PATH=/etc/ssl/certs/ca-certificates.crt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Meanwhile, the mentioned bug is fixed. But maybe you can't upgrade easily or quickly, so I hope this story helps some of you.&lt;/p&gt;

&lt;h1&gt;
  
  
  Further information
&lt;/h1&gt;

&lt;p&gt;gRPC isn't the only one affected by this. The older OpenSSL version 1.0.2 also stumbles. &lt;a href="https://dev.to/garutilorenzo/fix-let-s-encrypt-root-certificate-dst-root-ca-x3-expired-openssl-1-0-2-18f3"&gt;This post&lt;/a&gt; arrives at the same solution, and also has a nice collection of links for more background information.&lt;/p&gt;

</description>
      <category>ssl</category>
      <category>letsencrypt</category>
      <category>grpc</category>
    </item>
  </channel>
</rss>
