<?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: Mathew Gerald</title>
    <description>The latest articles on DEV Community by Mathew Gerald (@gerald-mathew).</description>
    <link>https://dev.to/gerald-mathew</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4038511%2F100f4dcf-4f0e-4815-b074-4b93847abace.png</url>
      <title>DEV Community: Mathew Gerald</title>
      <link>https://dev.to/gerald-mathew</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gerald-mathew"/>
    <language>en</language>
    <item>
      <title>I built a Supabase-style query client that talks directly to Postgres</title>
      <dc:creator>Mathew Gerald</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:25:21 +0000</pubDate>
      <link>https://dev.to/gerald-mathew/i-built-a-supabase-style-query-client-that-talks-directly-to-postgres-2m29</link>
      <guid>https://dev.to/gerald-mathew/i-built-a-supabase-style-query-client-that-talks-directly-to-postgres-2m29</guid>
      <description>&lt;p&gt;I like the Supabase JS client. &lt;code&gt;.from('todos').select('id, task').eq('done', false)&lt;/code&gt; reads like what I mean, and &lt;code&gt;{ data, error }&lt;/code&gt; means I'm not wrapping every query in try/catch.&lt;/p&gt;

&lt;p&gt;What I don't always want is the stuff around it: a project URL, an anon key, and a PostgREST service sitting between my Node server and a Postgres I already control.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;pg-fluent&lt;/strong&gt;: the &lt;code&gt;postgrest-js&lt;/code&gt; query builder, executed directly against PostgreSQL through &lt;code&gt;node-postgres&lt;/code&gt;.&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&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;pg-fluent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;connectionString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&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="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, task, done&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="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&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="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've used &lt;code&gt;supabase-js&lt;/code&gt;, that's already familiar. The difference is there's no HTTP request on the other end, just one parameterized SQL statement over a normal Postgres connection.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;pg-fluent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/gerald-mathew/pg-fluent" rel="noopener noreferrer"&gt;https://github.com/gerald-mathew/pg-fluent&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;People running long-lived servers (Express, Fastify, a VPS, containers) who like the Supabase query API but want to connect straight to Postgres, or who are moving off Supabase's hosted stack and don't want to rewrite every query. It's &lt;strong&gt;server-side only&lt;/strong&gt;, more on that below.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;I didn't reimplement the builder. pg-fluent uses the real &lt;code&gt;@supabase/postgrest-js&lt;/code&gt; and hands it a custom &lt;code&gt;fetch&lt;/code&gt;. When the builder "sends" a request, pg-fluent catches it, parses the URL, headers and body the way PostgREST would, plans one parameterized SQL statement, runs it through &lt;code&gt;pg&lt;/code&gt;, and returns a response &lt;code&gt;postgrest-js&lt;/code&gt; unpacks as if a server had answered.&lt;/p&gt;

&lt;p&gt;Results are aggregated to JSON inside Postgres, so you get the same shape as Supabase. Embeds like &lt;code&gt;select('id, title, author:users(name)')&lt;/code&gt; become subqueries that aggregate to JSON, with relationships resolved from the live schema (cached, with auto-refresh).&lt;/p&gt;

&lt;p&gt;Since I'm intercepting what the builder emits, an upstream change could quietly change the SQL. So the dependency is pinned to an exact version, and &lt;code&gt;docs/UPSTREAM.md&lt;/code&gt; records every bump with the date and reason. A bump that changes observable SQL or results is at least a minor release.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you get beyond the basics
&lt;/h2&gt;

&lt;p&gt;The whole &lt;code&gt;postgrest-js&lt;/code&gt; surface works: filters, &lt;code&gt;or()&lt;/code&gt;/&lt;code&gt;not()&lt;/code&gt;/&lt;code&gt;filter()&lt;/code&gt;, JSON paths like &lt;code&gt;meta-&amp;gt;&amp;gt;author&lt;/code&gt;, &lt;code&gt;range()&lt;/code&gt;, &lt;code&gt;single()&lt;/code&gt;/&lt;code&gt;maybeSingle()&lt;/code&gt;, &lt;code&gt;count&lt;/code&gt;, &lt;code&gt;upsert&lt;/code&gt;, &lt;code&gt;rpc&lt;/code&gt;, &lt;code&gt;csv()&lt;/code&gt;, &lt;code&gt;explain()&lt;/code&gt;, and embeds (many-to-one, one-to-many, many-to-many, &lt;code&gt;!inner&lt;/code&gt;, aliases, spread). Then the things that only make sense with a direct connection:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See the SQL without running it:&lt;/strong&gt;&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;todos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;done&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toSQL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Transactions with savepoints:&lt;/strong&gt;&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;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;trx&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;G&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;trx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profiles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isolationLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read committed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Raw SQL, safe by default.&lt;/strong&gt; The tagged template always binds interpolations as parameters. The only way to run SQL text you built yourself is &lt;code&gt;db.unsafe()&lt;/code&gt;, and the name is deliberate.&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;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="s2"&gt;`select * from users where id = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Streaming, COPY, LISTEN/NOTIFY:&lt;/strong&gt;&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="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;select&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="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyFrom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kind&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;csv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&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;payload&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;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;payload&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The listener gets its own non-pooled connection and reconnects with backoff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Named connections and read replicas.&lt;/strong&gt; Reads can go to replicas, while writes, RPC, transactions and LISTEN always hit the primary. Add &lt;code&gt;.primary()&lt;/code&gt; when you need read-your-writes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Type generation:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx pg-fluent gen-types &lt;span class="nt"&gt;--connection&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DATABASE_URL&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--schema&lt;/span&gt; public &lt;span class="nt"&gt;--out&lt;/span&gt; ./database.types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a &lt;code&gt;Database&lt;/code&gt; type compatible with &lt;code&gt;postgrest-js&lt;/code&gt;, so &lt;code&gt;createClient&amp;lt;Database&amp;gt;(...)&lt;/code&gt; just works.&lt;/p&gt;

&lt;p&gt;There are also &lt;code&gt;logger&lt;/code&gt; and &lt;code&gt;onQuery&lt;/code&gt; hooks and optional OpenTelemetry spans if &lt;code&gt;@opentelemetry/api&lt;/code&gt; is installed. Parameter values are never logged unless you opt in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails by default
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unfiltered &lt;code&gt;UPDATE&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt; is refused&lt;/strong&gt; with &lt;code&gt;PGFLUENT_UNFILTERED_MUTATION&lt;/code&gt;. Opt in with &lt;code&gt;.allowUnfiltered()&lt;/code&gt; or a client option.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reads are capped:&lt;/strong&gt; &lt;code&gt;maxRows&lt;/code&gt; defaults to 1000 when you set no limit or range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statements time out:&lt;/strong&gt; &lt;code&gt;statementTimeoutMs&lt;/code&gt; defaults to 30 seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries are read-only.&lt;/strong&gt; Transient connection failures retry with backoff for reads, and writes are never retried.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I tested it
&lt;/h2&gt;

&lt;p&gt;I didn't want to say "drop-in" on a hunch, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Differential tests against a real PostgREST server.&lt;/strong&gt; The same requests go through both, and the payloads must match. One difference is intentional and documented: a reserved parameter name like &lt;code&gt;columns&lt;/code&gt; or &lt;code&gt;select&lt;/code&gt; used as a filter is rejected with &lt;code&gt;PGFLUENT_INVALID_QUERY&lt;/code&gt;. PostgREST silently ignores it, which turns a scoped read into a full-table read.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Property-based injection tests&lt;/strong&gt; with &lt;code&gt;fast-check&lt;/code&gt;. Hostile strings go in as filter values, the SQL text must match the baseline, the values must land in parameters only, and a canary table must survive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A real CI matrix:&lt;/strong&gt; Node 22 and 24 against Postgres 15, 16, 17 and 18.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's 200+ tests. There's also a benchmark harness against raw &lt;code&gt;pg&lt;/code&gt;, but I'm &lt;strong&gt;not&lt;/strong&gt; publishing performance claims. Network and pooling change the numbers too much, so it only reports methodology and variance for you to run yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The RLS caveat (please read)
&lt;/h2&gt;

&lt;p&gt;pg-fluent runs statements as &lt;strong&gt;the connection's own role&lt;/strong&gt;, like &lt;code&gt;psql&lt;/code&gt; or any other driver. It doesn't set &lt;code&gt;request.jwt.claims&lt;/code&gt; and doesn't emulate Supabase's &lt;code&gt;anon&lt;/code&gt; and &lt;code&gt;authenticated&lt;/code&gt; roles.&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;RLS policies written for Supabase's roles don't automatically protect you here&lt;/strong&gt;. Connect as the table owner or a &lt;code&gt;BYPASSRLS&lt;/code&gt; role and RLS is skipped entirely. Use a least-privilege role, keep this on the server, and never let database credentials reach a browser. For per-user enforcement, connect as a non-owner role and set &lt;code&gt;SET ROLE&lt;/code&gt; and the JWT claims yourself. If you're migrating from &lt;code&gt;supabase-js&lt;/code&gt;, treat this as step zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's missing or rough
&lt;/h2&gt;

&lt;p&gt;It's &lt;code&gt;0.2.x&lt;/code&gt;, so, honestly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Embedding &lt;strong&gt;views&lt;/strong&gt; isn't supported yet (you can read them, just not embed them).&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;auth&lt;/code&gt;, &lt;code&gt;storage&lt;/code&gt; or &lt;code&gt;functions&lt;/code&gt;. It's a database client, not a BaaS SDK.&lt;/li&gt;
&lt;li&gt;LISTEN/NOTIFY doesn't work through transaction-mode poolers like PgBouncer. Use session-mode or a direct connection.&lt;/li&gt;
&lt;li&gt;Replica reads are asynchronous, and there's no lag measurement in v1.&lt;/li&gt;
&lt;li&gt;Requires Node 22+ and Postgres 15 to 18.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it, break it
&lt;/h2&gt;

&lt;p&gt;If you have a Supabase-style codebase and a Postgres you control, point &lt;code&gt;pg-fluent&lt;/code&gt; at a test database and tell me what breaks. Issues and PRs are welcome at &lt;a href="https://github.com/gerald-mathew/pg-fluent" rel="noopener noreferrer"&gt;https://github.com/gerald-mathew/pg-fluent&lt;/a&gt;, and security reports go through the private channel in &lt;code&gt;SECURITY.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I especially want to know which &lt;code&gt;postgrest-js&lt;/code&gt; queries behave differently from what you expect. Real queries find things my fixtures won't.&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>node</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a C++ CLI That Hunts Down Secrets Hiding in Your Git History</title>
      <dc:creator>Mathew Gerald</dc:creator>
      <pubDate>Wed, 02 Sep 2026 21:40:03 +0000</pubDate>
      <link>https://dev.to/gerald-mathew/i-built-a-c-cli-that-hunts-down-secrets-hiding-in-your-git-history-ac2</link>
      <guid>https://dev.to/gerald-mathew/i-built-a-c-cli-that-hunts-down-secrets-hiding-in-your-git-history-ac2</guid>
      <description>&lt;p&gt;We've all done it. You commit a &lt;code&gt;.env&lt;/code&gt; file by accident, catch it a few commits later, delete it, and breathe a sigh of relief. Except... it's not gone. It's still sitting in your Git history, one &lt;code&gt;git log -p&lt;/code&gt; away from anyone who clones the repo.&lt;/p&gt;

&lt;p&gt;That's the problem &lt;strong&gt;GitHub Secrets Watcher&lt;/strong&gt; is built to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;GitHub Secrets Watcher is a command-line tool written in modern C++20 that scans your GitHub repositories - public or private (with a token) - and walks back through commit history looking for accidentally committed environment and config files: &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;config.js&lt;/code&gt;, and similar patterns that often carry API keys, database credentials, or other secrets.&lt;/p&gt;

&lt;p&gt;For every hit, it gives you a direct link to the exact commit where the file appears, so you're not stuck digging through history manually to remediate it.&lt;/p&gt;

&lt;p&gt;Importantly, it's strictly &lt;strong&gt;read-only&lt;/strong&gt;. It never modifies, deletes, or rewrites anything in your repositories - it just reports what it finds so you can decide what to do next (rotate the secret, scrub the history, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  Key features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scans all repos for a given GitHub username, public by default, private too if you pass a token&lt;/li&gt;
&lt;li&gt;Walks a configurable depth of commit history across branches&lt;/li&gt;
&lt;li&gt;Skips noisy directories like &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, and &lt;code&gt;dist&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Validates commit hashes before generating links, so you never get a broken URL&lt;/li&gt;
&lt;li&gt;Multi-threaded scanning - set the thread count to match your hardware&lt;/li&gt;
&lt;li&gt;Three output formats: human-readable text (with color-coded terminal output), JSON, or CSV&lt;/li&gt;
&lt;li&gt;Stream-based processing to keep memory usage low even on large histories&lt;/li&gt;
&lt;li&gt;Modern, safe filesystem operations: Uses C++20 std::filesystem for all path handling, eliminating platform-specific path issues and improving security&lt;/li&gt;
&lt;li&gt;Direct Git integration: Leverages libgit2 for efficient, safe repository operations without spawning subprocesses&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Example output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[1/7] Scanning: ccas-website
[INFO] Cloning repository (depth=100)...
[INFO] Scanning history...
[WARN] Found 5 potential environment/configuration files:
     - client/assets/js/config.js
       [LINK] https://github.com/.../blob/c7e0fba.../client/assets/js/config.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each finding links straight to the offending commit, so remediation is one click away instead of a history-diving expedition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/gerald-mathew/github-secrets-watcher
&lt;span class="nb"&gt;cd &lt;/span&gt;github-secrets-watcher
&lt;span class="nb"&gt;mkdir &lt;/span&gt;build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;build
cmake ..
make
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run a scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./github_secrets_watcher scan &lt;span class="nt"&gt;--username&lt;/span&gt; YOUR_USERNAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a token for private repos and higher API rate limits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./github_secrets_watcher scan &lt;span class="nt"&gt;--username&lt;/span&gt; YOUR_USERNAME &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--token&lt;/span&gt; YOUR_PERSONAL_ACCESS_TOKEN &lt;span class="nt"&gt;--include-private&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can tune the scan with &lt;code&gt;--depth&lt;/code&gt;, &lt;code&gt;--max-repos&lt;/code&gt;, &lt;code&gt;--threads&lt;/code&gt;, &lt;code&gt;--format&lt;/code&gt;, &lt;code&gt;--verbose&lt;/code&gt;, &lt;code&gt;repo(s) &amp;lt;repo name(s)&amp;gt;&lt;/code&gt; and &lt;code&gt;--output&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why C++20
&lt;/h2&gt;

&lt;p&gt;This isn't a wrapper around &lt;code&gt;git log&lt;/code&gt; piped through &lt;code&gt;grep&lt;/code&gt; — it uses libcurl for GitHub API calls, libgit2 for direct Git repository access, nlohmann/json for parsing, and C++17 std::filesystem for safe, portable path operations, with modern C++20 features throughout. It builds cleanly with CMake or a provided build script on Linux, macOS, and Windows (via MSYS2).&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;

&lt;p&gt;It's MIT-licensed and open source. If you've got repos going back a few years, there's a decent chance something's buried in there that shouldn't be — I'd genuinely be curious what people find.&lt;/p&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/gerald-mathew/github-secrets-watcher" rel="noopener noreferrer"&gt;github.com/gerald-mathew/github-secrets-watcher&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback, issues, and PRs welcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Update - Sep 3:
&lt;/h2&gt;

&lt;p&gt;Based on feedback from the community, I’ve replaced the filesystem handling with std::filesystem and am now using libgit2 for Git operations instead of popen. Thanks to &lt;a class="mentioned-user" href="https://dev.to/hailiang194"&gt;@hailiang194&lt;/a&gt;  for the suggestions!&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>security</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
