<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Home on Philipp Steinrötter</title><link>/</link><description>Philipp Steinrötter (Home)</description><generator>Hugo -- gohugo.io</generator><language>en-En</language><managingEditor>philipp@steinroetter.com
(psteinroe)</managingEditor><lastBuildDate>Mon, 24 Feb 2025 00:00:00 +0000</lastBuildDate><atom:link href="/index.xml" rel="self" type="application/rss+xml"/><item><title>Postgres Async Triggers</title><link>/posts/postgres-async-triggers/</link><pubDate>Mon, 24 Feb 2025 00:00:00 +0000</pubDate><author>philipp@steinroetter.com (psteinroe)</author><guid>/posts/postgres-async-triggers/</guid><description>&lt;h1 id="postgres-async-triggers" >
&lt;div>
&lt;a href="#postgres-async-triggers">
##
&lt;/a>
Postgres Async Triggers
&lt;/div>
&lt;/h1>
&lt;p>Run asynchronous side-effects on database events.&lt;/p>
&lt;blockquote>
&lt;p>This blog post is accompanied by a demo repository on GitHub. Check it out &lt;a href="https://github.com/psteinroe/postgres-async-trigger-demo">here&lt;/a>.&lt;/p>&lt;/blockquote>
&lt;p>At &lt;a href="https://www.hellomateo.de">hellomateo&lt;/a>, we rely heavily on &lt;a href="https://supabase.com">Supabase&lt;/a>. And like any SaaS, we need to execute side-effects like sending a Webhook after a record changed. At first, we manually created database triggers that inserted jobs into the Postgres-based queue &lt;a href="https://worker.graphile.org">Graphile Worker&lt;/a> after insert/update/delete. Not only was this a bad DX, we also hit scalability issues: the fetch job query from Graphile Worker dominated our database load, and the maxed out workers were not able to process jobs fast enough during peak times.&lt;/p>
&lt;p>&lt;img src="https://philipp.steinroetter.com/images/sideeffects-before.png" alt="Async Trigger Before">&lt;/p>
&lt;p>As an intermediary solution, we forwarded the critical jobs from Graphile Worker to QStash from &lt;a href="https://upstash.com">Upstash&lt;/a>. This helped with the maxed out workers at the cost of higher latency. As a messaging solution, higher latency is bad though. But still, things stabilised for the time being which gave us time to find a better solution.&lt;/p>
&lt;p>Our goal was not only to solve the scalability issues, but also to improve DX. We wanted to reduce the surface area to write business logic. It should be easy to implement and test, and promote functional programming.&lt;/p>
&lt;h2 id="exploring-change-data-capture" >
&lt;div>
&lt;a href="#exploring-change-data-capture">
#
&lt;/a>
Exploring Change Data Capture
&lt;/div>
&lt;/h2>
&lt;p>First, we looked into the scalability issue, and explored how to get database events into workers efficiently. The obvious option is Change Data Capture (CDC). CDC is a mechanism based on Write-Ahead-Logging provided by Postgres, more specifically &lt;a href="https://www.postgresql.org/docs/current/logicaldecoding-explanation.html">logical decoding&lt;/a>. It allows us to subscribe to any change happening in the database. It’s almost instant, and without overhead. But it’s a complex beast. To better understand the protocol, I created a quick proof of concept for a pipeline that batches the events before sending them to to a HTTP endpoint. While the PoC was fun and the approach is feasible, there are a lot of things to take care of, and we did not want to take on that extra complexity.&lt;/p>
&lt;p>Next, we looked for tools that do the job for us. There is &lt;a href="https://github.com/cpursley/walex">WalEx&lt;/a>, an Elixir module to implement async triggers. Thats great, but we want to keep using Typescript. During that time, I also got in contact with &lt;a href="https://sequinstream.com">Sequin&lt;/a>. Their product is great, but its designed more as an infrastructure piece, and not towards application developers. We want to define our triggers in the code, and Sequin requires you to use their Dashboard or Terraform. There are other tools like &lt;a href="https://debezium.io">Debezium&lt;/a>, but their operational complexity is way too high. We decided that while CDC can be a great solution further down the road, its complexity is not justified yet.&lt;/p>
&lt;h2 id="the-simple-solution-is-good-enough" >
&lt;div>
&lt;a href="#the-simple-solution-is-good-enough">
#
&lt;/a>
The Simple Solution is Good Enough
&lt;/div>
&lt;/h2>
&lt;p>We took a step back, and began tinkering on improving what we already have (and know well): How can we reduce the database load and reduce latency without compromising on horizontal scalability?&lt;/p>
&lt;p>First, QStash had to be replaced. A HTTP based queue will always introduce latency we cannot afford. We quickly decided on &lt;a href="https://bullmq.io">BullMq&lt;/a>, because we were already using Redis for caching and it’s a battle-proven queuing system with low double digit latency.&lt;/p>
&lt;p>Now to the fun part: how to reduce the database load from Graphile Worker while increasing throughput? The only option is to start batching jobs. We have tables that trigger a lot of side effects, and each job was inserted from its own database trigger similar to this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-SQL" data-lang="SQL">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">-- first side effect on `my_table`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> &lt;span style="color:#ff6ac1">public&lt;/span>.first_side_effect() &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> perform private.add_graphile_worker_job (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;first_side_effect&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> to_json(&lt;span style="color:#ff6ac1">new&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> priority :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;high&amp;#39;&lt;/span>::private.graphile_worker_priority
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql &lt;span style="color:#ff6ac1">volatile&lt;/span> &lt;span style="color:#ff6ac1">set&lt;/span> search_path &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> first_side_effect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">after&lt;/span> &lt;span style="color:#ff6ac1">insert&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> my_table &lt;span style="color:#ff6ac1">for&lt;/span> &lt;span style="color:#ff6ac1">each&lt;/span> &lt;span style="color:#ff6ac1">row&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">when&lt;/span> (&lt;span style="color:#ff6ac1">new&lt;/span>.my_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">execute&lt;/span> &lt;span style="color:#ff6ac1">procedure&lt;/span> first_side_effect();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">-- second side effect on `my_table`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> &lt;span style="color:#ff6ac1">public&lt;/span>.second_side_effect() &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> perform private.add_graphile_worker_job (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;second_side_effect&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> to_json(&lt;span style="color:#ff6ac1">new&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> priority :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;high&amp;#39;&lt;/span>::private.graphile_worker_priority
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql &lt;span style="color:#ff6ac1">volatile&lt;/span> &lt;span style="color:#ff6ac1">set&lt;/span> search_path &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> second_side_effect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">after&lt;/span> &lt;span style="color:#ff6ac1">insert&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> my_table &lt;span style="color:#ff6ac1">for&lt;/span> &lt;span style="color:#ff6ac1">each&lt;/span> &lt;span style="color:#ff6ac1">row&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">when&lt;/span> (&lt;span style="color:#ff6ac1">new&lt;/span>.another_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">execute&lt;/span> &lt;span style="color:#ff6ac1">procedure&lt;/span> second_side_effect();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To use batching, we would need to have a single trigger that inserts a single batched job for each table and operation (&lt;code>AFTER [INSERT|UPDATE|DELETE]&lt;/code>).&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-SQL" data-lang="SQL">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> &lt;span style="color:#ff6ac1">public&lt;/span>.run_side_effects() &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">declare&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_payload json :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;[]&amp;#39;&lt;/span>::json
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>.my_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_payload :&lt;span style="color:#ff6ac1">=&lt;/span> v_payload &lt;span style="color:#ff6ac1">||&lt;/span> jsonb_build_object(&lt;span style="color:#5af78e">&amp;#39;tg_name&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;first_side_effect&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;new&amp;#39;&lt;/span>, to_json(&lt;span style="color:#ff6ac1">new&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>.another_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_payload :&lt;span style="color:#ff6ac1">=&lt;/span> v_payload &lt;span style="color:#ff6ac1">||&lt;/span> jsonb_build_object(&lt;span style="color:#5af78e">&amp;#39;tg_name&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;second_side_effect&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;new&amp;#39;&lt;/span>, to_json(&lt;span style="color:#ff6ac1">new&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> perform private.add_graphile_worker_job (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;side_effects&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_payload,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> priority :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;high&amp;#39;&lt;/span>::private.graphile_worker_priority
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql &lt;span style="color:#ff6ac1">volatile&lt;/span> &lt;span style="color:#ff6ac1">set&lt;/span> search_path &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> first_side_effect
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">after&lt;/span> &lt;span style="color:#ff6ac1">insert&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> my_table &lt;span style="color:#ff6ac1">for&lt;/span> &lt;span style="color:#ff6ac1">each&lt;/span> &lt;span style="color:#ff6ac1">row&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">when&lt;/span> (&lt;span style="color:#ff6ac1">new&lt;/span>.my_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>.another_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">execute&lt;/span> &lt;span style="color:#ff6ac1">procedure&lt;/span> first_side_effect();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here, we move the &lt;code>when&lt;/code> clauses into &lt;code>if&lt;/code> clauses, and add the job to the list of jobs if it evaluates to true. The Graphile Worker then picks up the batched job, and forwards the payloads based on the &lt;code>tg_name&lt;/code>. Note that this sample is missing a few important details, but more on that later.&lt;/p>
&lt;p>We did a quick proof of concept and the impact was significant.&lt;/p>
&lt;blockquote>
&lt;p>In our very first iteration, we had a single trigger function that used a lot of &lt;code>execute format&lt;/code> to dynamically build the payloads based on &lt;code>tg_name&lt;/code> and &lt;code>tg_op&lt;/code>, but that was even worse than not batching. Turns out, the execution context switch Postgres has to perform with &lt;code>execute format&lt;/code> is very costly.&lt;/p>&lt;/blockquote>
&lt;p>Now, how do we fix the DX? The database triggers are boilerplate, and we do not want to require a new migration every time we implement a side-effect anymore.&lt;/p>
&lt;h2 id="crafting-the-dx-implementing-async-triggers" >
&lt;div>
&lt;a href="#crafting-the-dx-implementing-async-triggers">
#
&lt;/a>
Crafting the DX: Implementing Async Triggers
&lt;/div>
&lt;/h2>
&lt;p>Our goal with &amp;ldquo;async triggers&amp;rdquo; is to have a single place where a developer implements, tests and registers side-effects. For each trigger, we want to bootstrap the DDL for the actual database trigger that checks the conditions for each subscription and inserts a batch job.&lt;/p>
&lt;p>The DSL we came up with is very similar to its equivalent in SQL:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">import&lt;/span> { builder } &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#39;../../builder&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">export&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> builder
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .createTrigger(&lt;span style="color:#5af78e">&amp;#39;my-side-effect&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .on(&lt;span style="color:#5af78e">&amp;#39;my_table&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .withColumns(&lt;span style="color:#5af78e">&amp;#39;column_a,column_b&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .afterInsert(&lt;span style="color:#5af78e">&amp;#34;new.column_a is true&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .afterUpdate(&lt;span style="color:#5af78e">&amp;#34;old.column_b is not true&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .afterDelete()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .execute(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">async&lt;/span> (payload, dependencies) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// full typed:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">// payload.new.column_a
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">// dependencies.supabaseClient.from()
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> { concurrency: &lt;span style="color:#9aedfe">300&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Any async trigger declares up to three subscriptions on a table: &lt;code>insert&lt;/code>, &lt;code>update&lt;/code> and &lt;code>delete&lt;/code>. Unlike database triggers, each subscription has its own optional &lt;code>when&lt;/code> clause to filter on old and new. We also enforce the selection of columns to reduce the payload size. The execute function receives a typed event payload as well as globally declared dependencies.&lt;/p>
&lt;blockquote>
&lt;p>The payload types are the only integration with Supabase. Everything else is just plain Postgres and Node.&lt;/p>&lt;/blockquote>
&lt;p>All functions are registered on the server and collected on startup of the service. Before we start processing jobs, a &lt;code>set_subscriptions&lt;/code> rpc is called that merges all subscriptions into the &lt;code>async_trigger.subscription&lt;/code> table:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> async_trigger.subscription (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id uuid &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> gen_random_uuid() &lt;span style="color:#ff6ac1">primary&lt;/span> &lt;span style="color:#ff6ac1">key&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> app_name &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">operation&lt;/span> async_trigger.operation_type &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>, &lt;span style="color:#78787e">-- INSERT, UPDATE, DELETE
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">schema_name&lt;/span> &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">table_name&lt;/span> &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> when_clause &lt;span style="color:#ff5c57">text&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> column_names &lt;span style="color:#ff5c57">text&lt;/span>[],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">unique&lt;/span> (name, &lt;span style="color:#ff6ac1">operation&lt;/span>, &lt;span style="color:#ff6ac1">schema_name&lt;/span>, &lt;span style="color:#ff6ac1">table_name&lt;/span>, app_name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The subscription table itself has a trigger that fires after insert, update and delete to sync the database triggers. It dynamically generates and executes the &lt;code>create function&lt;/code> and &lt;code>create trigger&lt;/code> statements to create one trigger for every operation and table. The generated DDL for the async trigger defined above looks somewhat like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> async_trigger._publish_events_after_update_on_my_table ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">inner&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">declare&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output jsonb :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;[]&amp;#39;&lt;/span>::jsonb;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- shared payload
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> v_base_payload jsonb :&lt;span style="color:#ff6ac1">=&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_op&amp;#39;&lt;/span>, tg_op,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_table_name&amp;#39;&lt;/span>, tg_table_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_table_schema&amp;#39;&lt;/span>, tg_table_schema,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;auth&amp;#39;&lt;/span>, jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;role&amp;#39;&lt;/span>, auth.&lt;span style="color:#ff6ac1">role&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;user_id&amp;#39;&lt;/span>, auth.uid()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;timestamp&amp;#39;&lt;/span>, (&lt;span style="color:#ff6ac1">extract&lt;/span>(epoch &lt;span style="color:#ff6ac1">from&lt;/span> now()) &lt;span style="color:#ff6ac1">*&lt;/span> &lt;span style="color:#ff9f43">1000&lt;/span>)::&lt;span style="color:#ff5c57">bigint&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">old&lt;/span>.column_b &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output :&lt;span style="color:#ff6ac1">=&lt;/span> v_jsonb_output &lt;span style="color:#ff6ac1">||&lt;/span> (jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_name&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;my-side-effect&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;new&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> tg_op &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">distinct&lt;/span> &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#39;DELETE&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;column_a&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">new&lt;/span>.column_a, &lt;span style="color:#5af78e">&amp;#39;column_b&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">new&lt;/span>.column_b
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;old&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> tg_op &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">distinct&lt;/span> &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#39;INSERT&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;column_a&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">new&lt;/span>.column_a, &lt;span style="color:#5af78e">&amp;#39;column_b&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">new&lt;/span>.column_b
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">||&lt;/span> v_base_payload);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- if ...some other clause then
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- v_jsonb_output := ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- end if;
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> jsonb_array_length(v_jsonb_output) &lt;span style="color:#ff6ac1">&amp;gt;&lt;/span> &lt;span style="color:#ff9f43">0&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> perform private.add_graphile_worker_job(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;publish_events&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output::json,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> priority :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;high&amp;#39;&lt;/span>::private.graphile_worker_priority
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> tg_op &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;DELETE&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">old&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">inner&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">language&lt;/span> plpgsql
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> async_trigger_after_update
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">after&lt;/span> &lt;span style="color:#ff6ac1">update&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> my_table &lt;span style="color:#ff6ac1">for&lt;/span> &lt;span style="color:#ff6ac1">each&lt;/span> &lt;span style="color:#ff6ac1">row&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">when&lt;/span> (&lt;span style="color:#ff6ac1">new&lt;/span>.my_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>.another_column &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">execute&lt;/span> &lt;span style="color:#ff6ac1">procedure&lt;/span> async_trigger._publish_events_after_update_on_my_table ();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>In the trigger function, we add an if block for every &amp;ldquo;subscription&amp;rdquo; and populate the payload with all relevant data. There is also payload shared by all subscriptions like the current user.&lt;/p>
&lt;p>The final job payload looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;auth&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;role&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;authenticated&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;user_id&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;xx&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;new&amp;#34;&lt;/span>:{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;column_a&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;value&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;column_b&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;value&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;tg_name&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;my-trigger&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;tg_op&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;INSERT&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;tg_table_name&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;event&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;tg_table_schema&amp;#34;&lt;/span>:&lt;span style="color:#5af78e">&amp;#34;public&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;timestamp&amp;#34;&lt;/span>:&lt;span style="color:#ff9f43">1739550115638&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, lets take a look at the &amp;ldquo;trigger that creates the triggers&amp;rdquo;. Its a bit more involved, so I added comments to help you go through it.&lt;/p>
&lt;details>
&lt;summary>See the trigger in its full glory&lt;/summary>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> async_trigger.sync_trigger()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> &lt;span style="color:#ff6ac1">as&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">declare&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_app_name &lt;span style="color:#ff5c57">text&lt;/span> :&lt;span style="color:#ff6ac1">=&lt;/span> coalesce(&lt;span style="color:#ff6ac1">new&lt;/span>.app_name, &lt;span style="color:#ff6ac1">old&lt;/span>.app_name);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_table_name &lt;span style="color:#ff5c57">text&lt;/span> :&lt;span style="color:#ff6ac1">=&lt;/span> coalesce(&lt;span style="color:#ff6ac1">new&lt;/span>.&lt;span style="color:#ff6ac1">table_name&lt;/span>, &lt;span style="color:#ff6ac1">old&lt;/span>.&lt;span style="color:#ff6ac1">table_name&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_schema_name &lt;span style="color:#ff5c57">text&lt;/span> :&lt;span style="color:#ff6ac1">=&lt;/span> coalesce(&lt;span style="color:#ff6ac1">new&lt;/span>.&lt;span style="color:#ff6ac1">schema_name&lt;/span>, &lt;span style="color:#ff6ac1">old&lt;/span>.&lt;span style="color:#ff6ac1">schema_name&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_when_clause &lt;span style="color:#ff5c57">text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_if_blocks &lt;span style="color:#ff5c57">text&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_op async_trigger.operation_type;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- every subscription can have all operations, so we need to iterate through them
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> foreach v_op &lt;span style="color:#ff6ac1">in&lt;/span> &lt;span style="color:#ff5c57">array&lt;/span> &lt;span style="color:#ff5c57">array&lt;/span>[&lt;span style="color:#5af78e">&amp;#39;INSERT&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;UPDATE&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;DELETE&amp;#39;&lt;/span>]::async_trigger.operation_type[] loop
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- clean up any existing trigger and function
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">execute&lt;/span> format(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">drop&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> async_trigger_after_&lt;span style="color:#ff6ac1">%&lt;/span>s&lt;span style="color:#ff6ac1">%&lt;/span>s &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">%&lt;/span>I.&lt;span style="color:#ff6ac1">%&lt;/span>I;&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>), v_test_postfix, v_schema_name, v_table_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">execute&lt;/span> format(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">drop&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> async_trigger._publish_events_after_&lt;span style="color:#ff6ac1">%&lt;/span>s_on_&lt;span style="color:#ff6ac1">%&lt;/span>s&lt;span style="color:#ff6ac1">%&lt;/span>s;&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>), v_table_name, v_test_postfix
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- check if we need to create a new trigger for the current operation
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> (&lt;span style="color:#ff6ac1">select&lt;/span> &lt;span style="color:#ff9f43">1&lt;/span> &lt;span style="color:#ff6ac1">from&lt;/span> async_trigger.subscription &lt;span style="color:#ff6ac1">where&lt;/span> &lt;span style="color:#ff6ac1">table_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_table_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">schema_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_schema_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">operation&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_op) &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- build the `WHEN` clause for the trigger
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- if there is at least one subscription for current operation without a when_clause, we do not add the when clause at all
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- otherwise, we combine them with OR, e.g. (old.col is true) OR (new.col is true)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> v_when_clause :&lt;span style="color:#ff6ac1">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> &lt;span style="color:#ff9f43">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">from&lt;/span> async_trigger.subscription
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">where&lt;/span> &lt;span style="color:#ff6ac1">table_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_table_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">schema_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_schema_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">operation&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_op &lt;span style="color:#ff6ac1">and&lt;/span> (when_clause &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> when_clause &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">and&lt;/span> app_name &lt;span style="color:#ff6ac1">=&lt;/span> v_app_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">then&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">else&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> string_agg(when_clause, &lt;span style="color:#5af78e">&amp;#39;) or (&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">from&lt;/span> async_trigger.subscription
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">where&lt;/span> &lt;span style="color:#ff6ac1">table_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_table_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">schema_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_schema_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">operation&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_op &lt;span style="color:#ff6ac1">and&lt;/span> when_clause &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">and&lt;/span> when_clause &lt;span style="color:#ff6ac1">!=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">and&lt;/span> app_name &lt;span style="color:#ff6ac1">=&lt;/span> v_app_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- build the if block for every subscription
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> v_if_blocks :&lt;span style="color:#ff6ac1">=&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> string_agg(format(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- the WHEN clause is put here
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">%&lt;/span>s &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output :&lt;span style="color:#ff6ac1">=&lt;/span> v_jsonb_output &lt;span style="color:#ff6ac1">||&lt;/span> (jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- the trigger name
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;tg_name&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">%&lt;/span>L,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- dynamically build the new and the old data from the selected `column_names`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;new&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> tg_op &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">distinct&lt;/span> &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#39;DELETE&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">%&lt;/span>s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;old&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> tg_op &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">distinct&lt;/span> &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#39;INSERT&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">%&lt;/span>s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- merge with the shared payload from below
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> ) &lt;span style="color:#ff6ac1">||&lt;/span> v_base_payload);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> coalesce(&lt;span style="color:#ff6ac1">nullif&lt;/span>(subscription.when_clause, &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;true&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subscription.destination,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> subscription.name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> string_agg(format(&lt;span style="color:#ff5c57">$&lt;/span>s$&lt;span style="color:#ff6ac1">%&lt;/span>L, &lt;span style="color:#ff6ac1">new&lt;/span>.&lt;span style="color:#ff6ac1">%&lt;/span>I$s$, &lt;span style="color:#ff6ac1">column_name&lt;/span>, &lt;span style="color:#ff6ac1">column_name&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;, &amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#ff6ac1">unnest&lt;/span>(subscription.column_names) &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff6ac1">column_name&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> string_agg(format(&lt;span style="color:#ff5c57">$&lt;/span>s$&lt;span style="color:#ff6ac1">%&lt;/span>L, &lt;span style="color:#ff6ac1">old&lt;/span>.&lt;span style="color:#ff6ac1">%&lt;/span>I$s$, &lt;span style="color:#ff6ac1">column_name&lt;/span>, &lt;span style="color:#ff6ac1">column_name&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;, &amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#ff6ac1">unnest&lt;/span>(subscription.column_names) &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff6ac1">column_name&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ), e&lt;span style="color:#5af78e">&amp;#39;\n&amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">from&lt;/span> async_trigger.subscription &lt;span style="color:#ff6ac1">where&lt;/span> &lt;span style="color:#ff6ac1">table_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_table_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">schema_name&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_schema_name &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">operation&lt;/span> &lt;span style="color:#ff6ac1">=&lt;/span> v_op &lt;span style="color:#ff6ac1">and&lt;/span> (v_is_test_app &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">false&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> app_name &lt;span style="color:#ff6ac1">=&lt;/span> v_app_name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- bootstrap the `CREATE FUNCTION` statement and execute it
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">execute&lt;/span> format(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> async_trigger._publish_events_after_&lt;span style="color:#ff6ac1">%&lt;/span>s_on_&lt;span style="color:#ff6ac1">%&lt;/span>s ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">inner&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">declare&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output jsonb :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;[]&amp;#39;&lt;/span>::jsonb;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_base_payload jsonb :&lt;span style="color:#ff6ac1">=&lt;/span> jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_op&amp;#39;&lt;/span>, tg_op,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_table_name&amp;#39;&lt;/span>, tg_table_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tg_table_schema&amp;#39;&lt;/span>, tg_table_schema,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;auth&amp;#39;&lt;/span>, jsonb_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;role&amp;#39;&lt;/span>, auth.&lt;span style="color:#ff6ac1">role&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;user_id&amp;#39;&lt;/span>, auth.uid()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;timestamp&amp;#39;&lt;/span>, (&lt;span style="color:#ff6ac1">extract&lt;/span>(epoch &lt;span style="color:#ff6ac1">from&lt;/span> now()) &lt;span style="color:#ff6ac1">*&lt;/span> &lt;span style="color:#ff9f43">1000&lt;/span>)::&lt;span style="color:#ff5c57">bigint&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- this is where we put all the if blocks
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">%&lt;/span>s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- if at least one subscription evaluates to true, insert the job
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> jsonb_array_length(v_jsonb_output) &lt;span style="color:#ff6ac1">&amp;gt;&lt;/span> &lt;span style="color:#ff9f43">0&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> perform private.add_graphile_worker_job(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;publish_events&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_jsonb_output::json,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> priority :&lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;high&amp;#39;&lt;/span>::private.graphile_worker_priority
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>p
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> tg_op &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;DELETE&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">old&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">inner&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_table_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_if_blocks
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- bootstrap the `CREATE TRIGGER` statement and execute it
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">execute&lt;/span> format(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">trigger&lt;/span> async_trigger_after_&lt;span style="color:#ff6ac1">%&lt;/span>s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">after&lt;/span> &lt;span style="color:#ff6ac1">%&lt;/span>s &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">%&lt;/span>I.&lt;span style="color:#ff6ac1">%&lt;/span>I
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">for&lt;/span> &lt;span style="color:#ff6ac1">each&lt;/span> &lt;span style="color:#ff6ac1">row&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">%&lt;/span>s
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- execute the function we created above
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">execute&lt;/span> &lt;span style="color:#ff6ac1">procedure&lt;/span> async_trigger._publish_events_after_&lt;span style="color:#ff6ac1">%&lt;/span>s_on_&lt;span style="color:#ff6ac1">%&lt;/span>s()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_schema_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_table_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> v_when_clause &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">and&lt;/span> &lt;span style="color:#ff6ac1">length&lt;/span>(v_when_clause) &lt;span style="color:#ff6ac1">&amp;gt;&lt;/span> &lt;span style="color:#ff9f43">0&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">then&lt;/span> &lt;span style="color:#5af78e">&amp;#39;when ((&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">||&lt;/span> v_when_clause &lt;span style="color:#ff6ac1">||&lt;/span> &lt;span style="color:#5af78e">&amp;#39;))&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">lower&lt;/span>(v_op::&lt;span style="color:#ff5c57">text&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> v_table_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> loop;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;/details>
&lt;p>Graphile Worker now simply iterates the jobs and forwards them to BullMQ. We use &lt;a href="https://github.com/graphile/worker/pull/474">this canary release&lt;/a> of Graphile Worker that implements local queues to maximise throughput and reduce database load at a theoretical latency cost. But we found that due to our single-digit job processing time and with at least two parallel workers, the downsides are negligible.&lt;/p>
&lt;h2 id="summary" >
&lt;div>
&lt;a href="#summary">
#
&lt;/a>
Summary
&lt;/div>
&lt;/h2>
&lt;p>&lt;img src="https://philipp.steinroetter.com/images/sideeffects-after.png" alt="Async Trigger After">&lt;/p>
&lt;p>And that’s it for the async triggers! We successfully reduced the load on our database and increased throughput by batching the export jobs and thanks to the local queue mode. The latency is very low too: the BullMQ worker is executed in less than 100ms after the transaction completes on the database. We are also happy with the DX. A new developer should be able to implement business logic quickly. And the best part: we are still using only the simple tools.&lt;/p>
&lt;h2 id="future-work" >
&lt;div>
&lt;a href="#future-work">
#
&lt;/a>
Future Work
&lt;/div>
&lt;/h2>
&lt;p>Right now, the framework is published as a shadcn-style demo. It is meant as a starting point for others to build their own. At some point, I hope to make this a more generic solution, where Graphile worker and BullMQ are just adapters for a generic „transport layer“. I would also love to see a variant that uses only Supabase infrastructure:&lt;/p>
&lt;ul>
&lt;li>&lt;code>pg_net&lt;/code> to get the jobs out of the database&lt;/li>
&lt;li>&lt;code>pg_mq&lt;/code> as the queue&lt;/li>
&lt;li>Edge Functions to execute the jobs&lt;/li>
&lt;/ul>
&lt;p>We did not go this path because at that time queues were not released yet, we don’t use edge functions, and this architecture would not fulfil our throughput and latency requirements. Still, it is a viable option for many, and I hope &lt;a href="https://www.pgflow.dev">PgFlow&lt;/a> can evolve into this.&lt;/p>
&lt;h2 id="additional-goodies" >
&lt;div>
&lt;a href="#additional-goodies">
#
&lt;/a>
Additional Goodies
&lt;/div>
&lt;/h2>
&lt;p>This blog post focusses on the async trigger implementation only. But the framework you can find in the demo repository is capable of more. We implemented an abstraction layer on top of BullMQ that allows for a boilerplate-free and type-safe implementation of business logic in a very functional manner. Functions &lt;a href="https://github.com/psteinroe/postgres-async-trigger-demo/blob/main/apps/functions-service/src/functions/contact/sample-trigger.ts">can call, trigger and schedule other functions&lt;/a>, and every handler receives globally defined dependencies that are created from the environment in a type-safe way. There is also a &lt;a href="https://github.com/psteinroe/postgres-async-trigger-demo/blob/main/apps/functions-service/src/functions/contact/sample-trigger.test.ts">test framework&lt;/a> to test both triggers and functions. Although a lot of things can still be polished, we are happy with its current status and hope it’s already of value for the community.&lt;/p>
&lt;p>If you want to help build this, we are hiring in Berlin! You can reach me on &lt;a href="mailto:philipp@hellomateo.de">philipp@hellomateo.de&lt;/a>.&lt;/p></description></item><item><title>Build A Public API with Supabase in 10 Minutes</title><link>/posts/public-api-with-supabase/</link><pubDate>Sat, 20 Apr 2024 00:00:00 +0000</pubDate><author>philipp@steinroetter.com (psteinroe)</author><guid>/posts/public-api-with-supabase/</guid><description>&lt;p>Building with Supabase means most likely you are not buildig your API yourself thanks to PostgREST and the GraphQL extension. But what if your customers want a public API to integrate with your product? Instead of building your own API server from scratch, you can leverage Postgres + PostgREST to build a public API in minutes. This blog post walks you through the required steps. The target architecture is very simple: A custom database role and Row-Level-Security, your PostgREST server, and a proxy that adds rate-limiting and observability, and a few additional security measures.&lt;/p>
&lt;p>&lt;img src="https://philipp.steinroetter.com/images/public-api-architecture.png" alt="Architecture">&lt;/p>
&lt;h2 id="the-database-schema" >
&lt;div>
&lt;a href="#the-database-schema">
#
&lt;/a>
The Database Schema
&lt;/div>
&lt;/h2>
&lt;p>First we create a sample schema. In this case, we create an &lt;code>organisation&lt;/code> table. Each organisation has employees. We also have a &lt;code>contact&lt;/code> table that we want to expose in our public API.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> organisation
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id uuid &lt;span style="color:#ff6ac1">primary&lt;/span> &lt;span style="color:#ff6ac1">key&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> gen_random_uuid(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">unique&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> employee
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id uuid &lt;span style="color:#ff6ac1">primary&lt;/span> &lt;span style="color:#ff6ac1">key&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> gen_random_uuid(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> organisation_id uuid &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">references&lt;/span> organisation &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">update&lt;/span> &lt;span style="color:#ff6ac1">restrict&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">delete&lt;/span> &lt;span style="color:#ff6ac1">cascade&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> private.organisation_id(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> user_id uuid &lt;span style="color:#ff6ac1">unique&lt;/span> &lt;span style="color:#ff6ac1">references&lt;/span> auth.users &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">update&lt;/span> &lt;span style="color:#ff6ac1">restrict&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">delete&lt;/span> &lt;span style="color:#ff6ac1">cascade&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">-- we want to expose this in our public api!
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> contact
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id uuid &lt;span style="color:#ff6ac1">primary&lt;/span> &lt;span style="color:#ff6ac1">key&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> gen_random_uuid(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> organisation_id uuid &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">references&lt;/span> organisation &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">delete&lt;/span> &lt;span style="color:#ff6ac1">cascade&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> private.organisation_id(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> full_name &lt;span style="color:#ff5c57">text&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We use Row-Level-Security to secure our data and make it accessible only if the user is authenticated and employee of the organisation. The &lt;code>private.organisation_id()&lt;/code> function is a little helper function that returns the &lt;code>organisation_id&lt;/code> of the authenticated user, if any.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> private.organisation_id() &lt;span style="color:#ff6ac1">returns&lt;/span> uuid &lt;span style="color:#ff6ac1">as&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">select&lt;/span> organisation_id &lt;span style="color:#ff6ac1">from&lt;/span> employee &lt;span style="color:#ff6ac1">where&lt;/span> user_id &lt;span style="color:#ff6ac1">=&lt;/span> auth.uid()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> &lt;span style="color:#ff6ac1">sql&lt;/span> &lt;span style="color:#ff6ac1">stable&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> policy employee_all &lt;span style="color:#ff6ac1">on&lt;/span> organisation &lt;span style="color:#ff6ac1">to&lt;/span> authenticated &lt;span style="color:#ff6ac1">using&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> private.organisation_id()) &lt;span style="color:#ff6ac1">=&lt;/span> id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> policy employee_all &lt;span style="color:#ff6ac1">on&lt;/span> employee &lt;span style="color:#ff6ac1">to&lt;/span> authenticated &lt;span style="color:#ff6ac1">using&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> private.organisation_id()) &lt;span style="color:#ff6ac1">=&lt;/span> organisation_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> policy employee_all &lt;span style="color:#ff6ac1">on&lt;/span> contact &lt;span style="color:#ff6ac1">to&lt;/span> authenticated &lt;span style="color:#ff6ac1">using&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> private.organisation_id()) &lt;span style="color:#ff6ac1">=&lt;/span> organisation_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="setup-api-tokens" >
&lt;div>
&lt;a href="#setup-api-tokens">
##
&lt;/a>
Setup API Tokens
&lt;/div>
&lt;/h3>
&lt;p>We will manage and create api tokens right within our database, and use Row-Level-Security to define what the api token can do. The first step is to create a role &lt;code>tokenauthed&lt;/code> that can be used to authenticate with the database. It should behave like the &lt;code>authenticated&lt;/code> role.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">role&lt;/span> tokenauthed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">grant&lt;/span> tokenauthed &lt;span style="color:#ff6ac1">to&lt;/span> authenticator;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">grant&lt;/span> anon &lt;span style="color:#ff6ac1">to&lt;/span> tokenauthed;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-api-token-table" >
&lt;div>
&lt;a href="#create-the-api-token-table">
##
&lt;/a>
Create the API Token Table
&lt;/div>
&lt;/h3>
&lt;p>We store references to api tokens in a table. Note that the key itself is not stored. Employees should be able to manage tokens.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> api_token
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id uuid &lt;span style="color:#ff6ac1">primary&lt;/span> &lt;span style="color:#ff6ac1">key&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> gen_random_uuid(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> organisation_id uuid &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">references&lt;/span> organisation &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">delete&lt;/span> &lt;span style="color:#ff6ac1">cascade&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> private.organisation_id(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> created_at &lt;span style="color:#ff6ac1">timestamp&lt;/span> &lt;span style="color:#ff6ac1">with&lt;/span> time &lt;span style="color:#ff6ac1">zone&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> now(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">table&lt;/span> api_token enable &lt;span style="color:#ff6ac1">row&lt;/span> &lt;span style="color:#ff6ac1">level&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> policy employee_all &lt;span style="color:#ff6ac1">on&lt;/span> api_token &lt;span style="color:#ff6ac1">to&lt;/span> authenticated &lt;span style="color:#ff6ac1">using&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> private.organisation_id()) &lt;span style="color:#ff6ac1">=&lt;/span> organisation_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="allow-users-to-actually-create-a-token" >
&lt;div>
&lt;a href="#allow-users-to-actually-create-a-token">
##
&lt;/a>
Allow users to actually create a token
&lt;/div>
&lt;/h3>
&lt;p>To actually create the api tokens we need to mint our own jwt tokens. We will use the &lt;code>pgjwt&lt;/code> extension for this. Luckily, its ready to install on our Supabase instance.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> extension &lt;span style="color:#ff6ac1">if&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">exists&lt;/span> &lt;span style="color:#5af78e">&amp;#34;pgjwt&amp;#34;&lt;/span> &lt;span style="color:#ff6ac1">with&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> &lt;span style="color:#5af78e">&amp;#34;extensions&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>create_api_token&lt;/code> function inserts an api token into the database, and then signs a jwt token with id of the token and the organisation id in the payload. There are a few notable things here. First, live supabase instances store the jwt_secret in the &lt;code>app.settings.jwt_secret&lt;/code> variable. &amp;gt;ou can run &lt;code>show app.settings.jwt_secret;&lt;/code> in the Supabase SQL editor for your project to prove this for yourself. The jwt secret for local development with the &lt;a href="https://supabase.com/docs/guides/local-development">Supabase CLI&lt;/a> has the constant value &amp;ldquo;super-secret-jwt-token-with-at-least-32-characters-long&amp;rdquo; as an undocumented &amp;ldquo;feature&amp;rdquo;. You might notice that the token is always valid forever for now. We will fix that at a later stage. And we use &lt;code>HS256&lt;/code>, because its the same algorithm Supabase auth uses.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> create_api_token (organisation_id uuid, name &lt;span style="color:#ff5c57">text&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">returns&lt;/span> &lt;span style="color:#ff5c57">text&lt;/span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>token$
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">declare&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> token_id uuid;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jwt_secret &lt;span style="color:#ff5c57">text&lt;/span> :&lt;span style="color:#ff6ac1">=&lt;/span> coalesce(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">nullif&lt;/span>(current_setting(&lt;span style="color:#5af78e">&amp;#39;app.settings.jwt_secret&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;super-secret-jwt-token-with-at-least-32-characters-long&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">insert&lt;/span> &lt;span style="color:#ff6ac1">into&lt;/span> api_token (organisation_id, name) &lt;span style="color:#ff6ac1">values&lt;/span> (organisation_id, name)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> returning id &lt;span style="color:#ff6ac1">into&lt;/span> token_id;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> extensions.sign(json_build_object(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;tid&amp;#39;&lt;/span>, token_id, &lt;span style="color:#5af78e">&amp;#39;iss&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;supabase&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;sub&amp;#39;&lt;/span>, organisation_id,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;role&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;tokenauthed&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;iat&amp;#39;&lt;/span>, trunc((&lt;span style="color:#ff6ac1">extract&lt;/span>(epoch &lt;span style="color:#ff6ac1">from&lt;/span> Now()))::&lt;span style="color:#ff5c57">numeric&lt;/span>, &lt;span style="color:#ff9f43">0&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jwt_secret::&lt;span style="color:#ff5c57">text&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;HS256&amp;#39;&lt;/span>::&lt;span style="color:#ff5c57">text&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>token$ &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">invoker&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> plpgsql;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="update-row-level-security-policies" >
&lt;div>
&lt;a href="#update-row-level-security-policies">
##
&lt;/a>
Update Row-Level-Security Policies
&lt;/div>
&lt;/h3>
&lt;p>First, we add a function to extract the &lt;code>tid&lt;/code> from the jwt token, similar to &lt;code>auth.uid()&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> private.tid ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> uuid &lt;span style="color:#ff6ac1">language&lt;/span> &lt;span style="color:#5af78e">&amp;#39;sql&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">stable&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>body$
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> coalesce(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">nullif&lt;/span> (current_setting(&lt;span style="color:#5af78e">&amp;#39;request.jwt.claim.tid&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">nullif&lt;/span> (current_setting(&lt;span style="color:#5af78e">&amp;#39;request.jwt.claims&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>)::jsonb &lt;span style="color:#ff6ac1">-&amp;gt;&amp;gt;&lt;/span> &lt;span style="color:#5af78e">&amp;#39;tid&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )::uuid
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>body$;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We then update &lt;code>private.organisation_id()&lt;/code> to return the &lt;code>organisation_id&lt;/code> from the jwt payload when the role is &lt;code>tokenauthed&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> private.organisation_id() &lt;span style="color:#ff6ac1">returns&lt;/span> uuid &lt;span style="color:#ff6ac1">as&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">case&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">when&lt;/span> auth.&lt;span style="color:#ff6ac1">role&lt;/span>() &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;authenticated&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">then&lt;/span> (&lt;span style="color:#ff6ac1">select&lt;/span> organisation_id &lt;span style="color:#ff6ac1">from&lt;/span> employee &lt;span style="color:#ff6ac1">where&lt;/span> user_id &lt;span style="color:#ff6ac1">=&lt;/span> auth.uid())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">when&lt;/span> auth.&lt;span style="color:#ff6ac1">role&lt;/span>() &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;tokenauthed&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">then&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span> coalesce(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">nullif&lt;/span> (current_setting(&lt;span style="color:#5af78e">&amp;#39;request.jwt.claim.sub&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">nullif&lt;/span> (current_setting(&lt;span style="color:#5af78e">&amp;#39;request.jwt.claims&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>), &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>)::jsonb &lt;span style="color:#ff6ac1">-&amp;gt;&amp;gt;&lt;/span> &lt;span style="color:#5af78e">&amp;#39;sub&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )::uuid
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>&lt;span style="color:#ff6ac1">sql&lt;/span>&lt;span style="color:#ff5c57">$&lt;/span> &lt;span style="color:#ff6ac1">language&lt;/span> &lt;span style="color:#ff6ac1">sql&lt;/span> &lt;span style="color:#ff6ac1">stable&lt;/span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, we re-create the Row-Level-Security policy on &lt;code>contact&lt;/code> to include &lt;code>tokenauthed&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">drop&lt;/span> policy employee_all &lt;span style="color:#ff6ac1">on&lt;/span> contact;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> policy employee_tokenauthed_all &lt;span style="color:#ff6ac1">on&lt;/span> contact &lt;span style="color:#ff6ac1">to&lt;/span> authenticated, tokenauthed &lt;span style="color:#ff6ac1">using&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> (&lt;span style="color:#ff6ac1">select&lt;/span> private.organisation_id()) &lt;span style="color:#ff6ac1">=&lt;/span> organisation_id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="create-the-api-schema" >
&lt;div>
&lt;a href="#create-the-api-schema">
##
&lt;/a>
Create the API Schema
&lt;/div>
&lt;/h3>
&lt;p>To control what tables and columns should be exposed, we create a separate &lt;code>api&lt;/code> schema. This separates our internal data model from the data model exposed in our public api.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> api;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">grant&lt;/span> &lt;span style="color:#ff6ac1">usage&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> api &lt;span style="color:#ff6ac1">to&lt;/span> postgres, anon, authenticated, service_role, tokenauthed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> &lt;span style="color:#ff6ac1">privileges&lt;/span> &lt;span style="color:#ff6ac1">in&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> api &lt;span style="color:#ff6ac1">grant&lt;/span> &lt;span style="color:#ff6ac1">all&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> tables &lt;span style="color:#ff6ac1">to&lt;/span> postgres, anon, authenticated, service_role, tokenauthed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> &lt;span style="color:#ff6ac1">privileges&lt;/span> &lt;span style="color:#ff6ac1">in&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> api &lt;span style="color:#ff6ac1">grant&lt;/span> &lt;span style="color:#ff6ac1">all&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> functions &lt;span style="color:#ff6ac1">to&lt;/span> postgres, anon, authenticated, service_role, tokenauthed;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> &lt;span style="color:#ff6ac1">privileges&lt;/span> &lt;span style="color:#ff6ac1">in&lt;/span> &lt;span style="color:#ff6ac1">schema&lt;/span> api &lt;span style="color:#ff6ac1">grant&lt;/span> &lt;span style="color:#ff6ac1">all&lt;/span> &lt;span style="color:#ff6ac1">on&lt;/span> sequences &lt;span style="color:#ff6ac1">to&lt;/span> postgres, anon, authenticated, service_role, tokenauthed;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We now use &lt;a href="https://www.postgresql.org/docs/current/sql-createview.html">&amp;ldquo;updateable views&amp;rdquo;&lt;/a> and the &lt;code>security_invoker&lt;/code> config to expose the &lt;code>contact&lt;/code>. An updatable view in postgres is a view which allows mutations, because the underlying table can be derived. Thanks to the &lt;code>security_invoker&lt;/code> config, all operations will respect the Row-Level-Security policy we set on the &lt;code>contact&lt;/code> table. For better performance, we always filter on the &lt;code>organisation_id&lt;/code> and therefore mimick the security policy clause. With the view, we can also define what columns should be exposed. This is pretty neat, since most apps will have columns that are internal to the app logic.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">view&lt;/span> api.contact &lt;span style="color:#ff6ac1">with&lt;/span> (security_invoker) &lt;span style="color:#ff6ac1">as&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">select&lt;/span> id, full_name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">from&lt;/span> contact
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">where&lt;/span> organisation_id &lt;span style="color:#ff6ac1">=&lt;/span> private.organisation_id();
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And thats it for the database part. As of now, we have:&lt;/p>
&lt;ul>
&lt;li>a custom database role&lt;/li>
&lt;li>a function to generate tokens&lt;/li>
&lt;li>helper functions to easily define Row-Level-Security Policies&lt;/li>
&lt;li>a table that is exposed via a custom &lt;code>api&lt;/code> schema&lt;/li>
&lt;/ul>
&lt;p>We could just share our Supabase Rest API endpoint and be done. I would not recommend that. Instead, lets build a little proxy that will give us the possibility to add required security measures such as rate-limiting and observability.&lt;/p>
&lt;h2 id="the-proxy" >
&lt;div>
&lt;a href="#the-proxy">
#
&lt;/a>
The Proxy
&lt;/div>
&lt;/h2>
&lt;p>Our little proxy will be a very simple Cloudflare Worker using Hono. You can choose any framework or platform you want for this though. First, we set up the project.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-shell" data-lang="shell">&lt;span style="display:flex;">&lt;span>bunx create-hono api-proxy
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>First, setup a JWT middleware to verify the incoming request. You can find the JWT secret in your Supabase Dashboard.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> app &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Hono&amp;lt;&lt;span style="color:#ff6ac1">Env&lt;/span>&amp;gt;();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.use((c, next) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> jwtMiddleware &lt;span style="color:#ff6ac1">=&lt;/span> jwt({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> secret: &lt;span style="color:#9aedfe">c.env.SUPABASE_JWT_SECRET&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> alg&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#34;HS256&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> });
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> jwtMiddleware(c, next);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now, add a single &amp;ldquo;catch-all&amp;rdquo; route that forwards the request to the upstream PostgREST api.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>app.all(&lt;span style="color:#5af78e">&amp;#34;/:table&amp;#34;&lt;/span>, &lt;span style="color:#ff6ac1">async&lt;/span> (c) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> searchParams &lt;span style="color:#ff6ac1">=&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> c.req.url.indexOf(&lt;span style="color:#5af78e">&amp;#34;?&amp;#34;&lt;/span>) &lt;span style="color:#ff6ac1">&amp;gt;&lt;/span> &lt;span style="color:#ff6ac1">-&lt;/span>&lt;span style="color:#ff9f43">1&lt;/span> &lt;span style="color:#ff6ac1">?&lt;/span> c.req.url.slice(c.req.url.indexOf(&lt;span style="color:#5af78e">&amp;#34;?&amp;#34;&lt;/span>)) &lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#34;&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> upstreamUrl &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">`&lt;/span>&lt;span style="color:#5af78e">${&lt;/span>c.env.SUPABASE_URL&lt;span style="color:#5af78e">}&lt;/span>&lt;span style="color:#5af78e">/rest/v1/&lt;/span>&lt;span style="color:#5af78e">${&lt;/span>c.req.param(&lt;span style="color:#5af78e">&amp;#34;table&amp;#34;&lt;/span>)&lt;span style="color:#5af78e">}${&lt;/span>searchParams&lt;span style="color:#5af78e">}&lt;/span>&lt;span style="color:#5af78e">`&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> newRequest &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Request(upstreamUrl, c.req.raw);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> res &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">await&lt;/span> fetch(newRequest);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> newResponse &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Response(res.body, res);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> newResponse;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To fail early, you might want to validate the path parameter.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">import&lt;/span> { &lt;span style="color:#9aedfe">object&lt;/span>, picklist } &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#34;valibot&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">import&lt;/span> { vValidator } &lt;span style="color:#ff6ac1">from&lt;/span> &lt;span style="color:#5af78e">&amp;#34;@hono/valibot-validator&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> TABLES &lt;span style="color:#ff6ac1">=&lt;/span> [&lt;span style="color:#5af78e">&amp;#34;contact&amp;#34;&lt;/span>];
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> paramsSchema &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#9aedfe">object&lt;/span>({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> table: &lt;span style="color:#9aedfe">picklist&lt;/span>(TABLES),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>});
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>app.all(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;/:table&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vValidator&amp;lt;&lt;span style="color:#ff6ac1">typeof&lt;/span> &lt;span style="color:#57c7ff">paramsSchema&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, &lt;span style="color:#57c7ff">Env&lt;/span>, &lt;span style="color:#57c7ff">any&lt;/span>&amp;gt;(&lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, paramsSchema),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">async&lt;/span> (c) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For Kong, the API gateway that Supabase uses, to accept your proxied request, we need to set the &lt;code>apiKey&lt;/code> header to the &lt;code>anon&lt;/code> key. To target the &lt;code>api&lt;/code> schema, set the &lt;code>Accept-Profile&lt;/code> and &lt;code>Content-Profile&lt;/code> headers.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>app.all(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;/:table&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vValidator&amp;lt;&lt;span style="color:#ff6ac1">typeof&lt;/span> &lt;span style="color:#57c7ff">paramsSchema&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, &lt;span style="color:#57c7ff">Env&lt;/span>, &lt;span style="color:#57c7ff">any&lt;/span>&amp;gt;(&lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, paramsSchema),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">async&lt;/span> (c) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">const&lt;/span> newRequest &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Request(upstreamUrl, c.req.raw);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newRequest.headers.&lt;span style="color:#ff6ac1">set&lt;/span>(&lt;span style="color:#5af78e">&amp;#34;apiKey&amp;#34;&lt;/span>, c.env.SUPABASE_ANON_KEY);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> ([&lt;span style="color:#5af78e">&amp;#34;GET&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;HEAD&amp;#34;&lt;/span>].includes(c.req.method)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newRequest.headers.&lt;span style="color:#ff6ac1">set&lt;/span>(&lt;span style="color:#5af78e">&amp;#34;Accept-Profile&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;api&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> } &lt;span style="color:#ff6ac1">else&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newRequest.headers.&lt;span style="color:#ff6ac1">set&lt;/span>(&lt;span style="color:#5af78e">&amp;#34;Content-Profile&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;api&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is also a great opportunity to change the default behavior of PostgREST. For example, your users might expect that any mutation always returns the mutated row. This can be enabled by default by always adding &lt;code>return=representation&lt;/code> to the &lt;code>Prefer&lt;/code> header.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>app.all(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;/:table&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vValidator&amp;lt;&lt;span style="color:#ff6ac1">typeof&lt;/span> &lt;span style="color:#57c7ff">paramsSchema&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, &lt;span style="color:#57c7ff">Env&lt;/span>, &lt;span style="color:#57c7ff">any&lt;/span>&amp;gt;(&lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, paramsSchema),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">async&lt;/span> (c) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">const&lt;/span> newRequest &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Request(upstreamUrl, c.req.raw);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> (c.req.query(&lt;span style="color:#5af78e">&amp;#34;select&amp;#34;&lt;/span>)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newRequest.headers.&lt;span style="color:#ff6ac1">set&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;Prefer&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [c.req.header(&lt;span style="color:#5af78e">&amp;#34;Prefer&amp;#34;&lt;/span>), &lt;span style="color:#5af78e">&amp;#34;return=representation&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .filter(&lt;span style="color:#ff5c57">Boolean&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .join(&lt;span style="color:#5af78e">&amp;#34;,&amp;#34;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And thats it! From here, you should be able to complete this on your own. To be ready for production, you should add rate-limiting as well as logging and monitoring to your worker.&lt;/p>
&lt;h2 id="recommendations" >
&lt;div>
&lt;a href="#recommendations">
#
&lt;/a>
Recommendations
&lt;/div>
&lt;/h2>
&lt;p>Control the statement timeout for your &lt;code>tokenauthed&lt;/code> role. You are giving your users a lot of power with all the query features that PostgREST offers. To make sure they spend some time optimizing their requests, set the limit to a relatively low value depending on your use case.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">role&lt;/span> tokenauthed &lt;span style="color:#ff6ac1">set&lt;/span> statement_timeout &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;2s&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, you should make sure that we can invalidate tokens. For now, a token can be invalidated if its reference in the &lt;code>api_token&lt;/code> table is deleted. We can leverage the &lt;code>db_pre_request&lt;/code> hook that PostgREST exposes for this. To read more about the Pre-Request feature check out their &lt;a href="https://postgrest.org/en/v12/references/transactions.html#pre-request">documentation&lt;/a>. Its basically a middleware within Postgres which we can use to ensure that the token reference in our database still exists.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> private.validate_token ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">language&lt;/span> &lt;span style="color:#5af78e">&amp;#39;plpgsql&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>body$
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- 1. we should only verify api tokens.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- skip check if auth is not done using an api token
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> auth.&lt;span style="color:#ff6ac1">role&lt;/span> () &lt;span style="color:#ff6ac1">&amp;lt;&amp;gt;&lt;/span> &lt;span style="color:#5af78e">&amp;#39;tokenauthed&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- 2. make sure the token has not been revoked
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> (&lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">from&lt;/span> api_token t
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">where&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id &lt;span style="color:#ff6ac1">=&lt;/span> private.tid ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> &lt;span style="color:#ff6ac1">false&lt;/span> &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>) &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> raise &lt;span style="color:#ff6ac1">sqlstate&lt;/span> &lt;span style="color:#5af78e">&amp;#39;pt401&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">using&lt;/span> message &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;unauthorized&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>body$;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">alter&lt;/span> &lt;span style="color:#ff6ac1">role&lt;/span> authenticator &lt;span style="color:#ff6ac1">set&lt;/span> pgrst.db_pre_request &lt;span style="color:#ff6ac1">to&lt;/span> &lt;span style="color:#5af78e">&amp;#39;private.validate_token&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">notify&lt;/span> pgrst, &lt;span style="color:#5af78e">&amp;#39;reload config&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Finally, we only want to allow &lt;code>tokenauthed&lt;/code> requests if they were routed through our proxy, so that rate-limiting applies. The simplest way to achieve this is to add a custom secret value to the header in the request.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>app.all(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;/:table&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> vValidator&amp;lt;&lt;span style="color:#ff6ac1">typeof&lt;/span> &lt;span style="color:#57c7ff">paramsSchema&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, &lt;span style="color:#57c7ff">Env&lt;/span>, &lt;span style="color:#57c7ff">any&lt;/span>&amp;gt;(&lt;span style="color:#5af78e">&amp;#34;param&amp;#34;&lt;/span>, paramsSchema),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">async&lt;/span> (c) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">const&lt;/span> newRequest &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#ff6ac1">new&lt;/span> Request(upstreamUrl, c.req.raw);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> (c.req.query(&lt;span style="color:#5af78e">&amp;#34;select&amp;#34;&lt;/span>)) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> newRequest.headers.&lt;span style="color:#ff6ac1">set&lt;/span>(&lt;span style="color:#5af78e">&amp;#34;x-proxy-secret&amp;#34;&lt;/span>, c.env.PROXY_SECRET);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We can check for the existence of this token within the &lt;code>validate_token&lt;/code> function. Luckily, our Supabase project comes with a built-in Vault, so we do not have to worry about storing our secret key in the database.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">create&lt;/span> &lt;span style="color:#ff6ac1">or&lt;/span> &lt;span style="color:#ff6ac1">replace&lt;/span> &lt;span style="color:#ff6ac1">function&lt;/span> private.validate_token ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">returns&lt;/span> void
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">language&lt;/span> &lt;span style="color:#5af78e">&amp;#39;plpgsql&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">security&lt;/span> &lt;span style="color:#ff6ac1">definer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">as&lt;/span> &lt;span style="color:#ff5c57">$&lt;/span>body$
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">begin&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- 1. we should only verify api tokens.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#78787e">-- skip check if auth is not done using an api token
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> auth.&lt;span style="color:#ff6ac1">role&lt;/span> () &lt;span style="color:#ff6ac1">&amp;lt;&amp;gt;&lt;/span> &lt;span style="color:#5af78e">&amp;#39;tokenauthed&amp;#39;&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- 2. Make sure that the request is going through our proxy
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> (&lt;span style="color:#ff6ac1">select&lt;/span> current_setting(&lt;span style="color:#5af78e">&amp;#39;request.headers&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>)::json &lt;span style="color:#ff6ac1">-&amp;gt;&amp;gt;&lt;/span> &lt;span style="color:#5af78e">&amp;#39;x-proxy-secret&amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">&amp;lt;&amp;gt;&lt;/span> (&lt;span style="color:#ff6ac1">select&lt;/span> decrypted_secret &lt;span style="color:#ff6ac1">from&lt;/span> vault.decrypted_secrets &lt;span style="color:#ff6ac1">where&lt;/span> name &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;proxy_secret&amp;#39;&lt;/span>) &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> raise &lt;span style="color:#ff6ac1">sqlstate&lt;/span> &lt;span style="color:#5af78e">&amp;#39;pt401&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">using&lt;/span> message &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;unauthorized&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">-- 3. make sure the token has not been revoked
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span> (&lt;span style="color:#ff6ac1">case&lt;/span> &lt;span style="color:#ff6ac1">when&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">select&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">from&lt;/span> api_token t
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">where&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id &lt;span style="color:#ff6ac1">=&lt;/span> private.tid ()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ) &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">not&lt;/span> &lt;span style="color:#ff6ac1">null&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span> &lt;span style="color:#ff6ac1">false&lt;/span> &lt;span style="color:#ff6ac1">else&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">end&lt;/span>) &lt;span style="color:#ff6ac1">is&lt;/span> &lt;span style="color:#ff6ac1">true&lt;/span> &lt;span style="color:#ff6ac1">then&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> raise &lt;span style="color:#ff6ac1">sqlstate&lt;/span> &lt;span style="color:#5af78e">&amp;#39;pt401&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">using&lt;/span> message &lt;span style="color:#ff6ac1">=&lt;/span> &lt;span style="color:#5af78e">&amp;#39;unauthorized&amp;#39;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">end&lt;/span> &lt;span style="color:#ff6ac1">if&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">end&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">$&lt;/span>body$;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And thats it!&lt;/p>
&lt;p>You can find the source code and a demo on &lt;a href="https://github.com/psteinroe/supabase-public-api-demo">GitHub&lt;/a>.&lt;/p></description></item><item><title>Announcing Supabase Cache Helpers 1.0</title><link>/posts/supabase-cache-helpers-v1/</link><pubDate>Tue, 25 Apr 2023 00:00:00 +0000</pubDate><author>philipp@steinroetter.com (psteinroe)</author><guid>/posts/supabase-cache-helpers-v1/</guid><description>&lt;p>&lt;a href="https://supabase-cache-helpers.vercel.app">Supabase Cache Helpers&lt;/a> is a collection of framework specific cache utilities for working with &lt;a href="https://supabase.com">Supabase&lt;/a>. It bridges the gap between data-fetching libraries such as &lt;a href="https://swr.vercel.app">SWR&lt;/a> and &lt;a href="https://tanstack.com/query/v3/docs/react/overview">React Query&lt;/a>, and the Supabase client libraries.&lt;/p>
&lt;h2 id="motivation" >
&lt;div>
&lt;a href="#motivation">
#
&lt;/a>
Motivation
&lt;/div>
&lt;/h2>
&lt;p>Supabase offers a bunch of SDKs to interact with their services, such as the Postgres database, Realtime and Storage. Depending on your requirements, these might be sufficient to build your application. At some point however, you will want to use data-fetching libraries such as SWR and React Query to reduce boilerplate, cache data, dedupe requests, and much more. When starting to use these libraries, you might find that its quite straightforward to fetch data from Supabase: define a cache key, write a simple fetcher functions and off you go. Here is an example of a simple hook that fetches a &lt;code>note&lt;/code> by id using a SWR hook:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">// fetcher function to get contact by id
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff6ac1">export&lt;/span> &lt;span style="color:#ff5c57">function&lt;/span> getContactById(client: &lt;span style="color:#9aedfe">SupabaseClient&lt;/span>, contactId: &lt;span style="color:#9aedfe">string&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> client
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .select(&lt;span style="color:#5af78e">`id,username,ticket_number`&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .eq(&lt;span style="color:#5af78e">&amp;#39;id&amp;#39;&lt;/span>, contactId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .throwOnError()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .single()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">// wrapper hook
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff5c57">function&lt;/span> useContactQuery(contactId: &lt;span style="color:#9aedfe">string&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> client &lt;span style="color:#ff6ac1">=&lt;/span> useSupabaseClient()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// cache key for this query
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">const&lt;/span> key &lt;span style="color:#ff6ac1">=&lt;/span> [&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>, contactId]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> useSWR(key, &lt;span style="color:#ff6ac1">async&lt;/span> () &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> getContactById(client, contactId).then((result) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> result.data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">export&lt;/span> &lt;span style="color:#ff6ac1">default&lt;/span> useContactQuery
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Easy enough. Now import the hook into your component and display the contact.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff5c57">function&lt;/span> ContactCard({ contactId }&lt;span style="color:#ff6ac1">:&lt;/span> { contactId: &lt;span style="color:#9aedfe">string&lt;/span> }) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> { data: &lt;span style="color:#9aedfe">contact&lt;/span>, isLoading, isError } &lt;span style="color:#ff6ac1">=&lt;/span> useContactQuery(contactId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> (isLoading) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &amp;lt;&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;Loading...&amp;lt;/&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> (isError) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> &amp;lt;&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;&lt;span style="color:#ff5c57">Error&lt;/span>&amp;lt;/&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;&lt;span style="color:#ff6ac1">h1&lt;/span>&amp;gt;{contact.username}&amp;lt;/&lt;span style="color:#ff6ac1">h1&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &amp;lt;/&lt;span style="color:#ff6ac1">div&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>But its a lot of boilerplate, right? And as a developer, you have little transparency on the actual query being executed when looking at the &lt;code>ContactNote&lt;/code> component. In our organisation, we found this intransparency to be the source of performance issues and bugs, because people tend to avoid writing a new hook, and just overload that one query with all the columns, filters and joins they need. This becomes worse when the size and complexity of your app grows. Imagine a complex table view, that allows the user to apply a bunch of filters and ordering. For proper caching, you will need to pass filter object into your hook, and encode the query settings into the key. The maintainability will decrease to zero at some point.&lt;/p>
&lt;p>The same is true for mutations. It is simple at first. Just define your fetcher, and pass it to the &lt;code>useMutation&lt;/code> hook:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">// fetcher function to update a note by id
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff6ac1">export&lt;/span> &lt;span style="color:#ff6ac1">async&lt;/span> &lt;span style="color:#ff5c57">function&lt;/span> updateContact(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client: &lt;span style="color:#9aedfe">SupabaseClient&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id: &lt;span style="color:#9aedfe">string&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> data: &lt;span style="color:#9aedfe">Partial&lt;/span>&amp;lt;&lt;span style="color:#ff6ac1">Contact&lt;/span>&amp;gt;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> client
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .update(data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .eq(&lt;span style="color:#5af78e">&amp;#39;id&amp;#39;&lt;/span>, id)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .throwOnError()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .select(&lt;span style="color:#5af78e">&amp;#39;id,username&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .throwOnError()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .single()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">// wrapper hook
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>&lt;span style="color:#ff5c57">function&lt;/span> useUpdateContactMutation() {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">const&lt;/span> client &lt;span style="color:#ff6ac1">=&lt;/span> useSupabase()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> useMutation(&lt;span style="color:#ff6ac1">async&lt;/span> (contact: &lt;span style="color:#9aedfe">Contact&lt;/span>) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> updateContactById(client, note).then((result) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> result.data)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Easy. When updating the username of the contact, you might want the change to be reflected both in the &lt;code>ContactCard&lt;/code> component, as well as in the table view. Oversimplified, such cache update code might look like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> onSuccess&lt;span style="color:#ff6ac1">:&lt;/span> (updatedContact) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// update query that selects a single contact by id
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> mutate([&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>, updatedContact.id], (existingContact) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...existingContact,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...updatedContact,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// you might have a contact list query somewhere, too.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> mutate([&lt;span style="color:#5af78e">&amp;#39;contacts&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;filters=has_golden_ticket.is.true&amp;#39;&lt;/span>], (data) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> (data) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> data.map((contact) &lt;span style="color:#ff6ac1">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">if&lt;/span> (updatedContact.id &lt;span style="color:#ff6ac1">===&lt;/span> contact.id) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> { ...contact, ...updatedContact }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">return&lt;/span> contact
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is a very simple example, and does not nearly cover all required edge cases. For example, what if the table query is filtered or ordered on the username of the contact? You will need to write a lot of repetitive code, just to keep your apps cache up-to-date. But what if we could implement a generalizable solution?&lt;/p>
&lt;p>With Supabase Cache Helpers, you can archieve all of the above and more with just a single line of code.&lt;/p>
&lt;h2 id="encoding-queries" >
&lt;div>
&lt;a href="#encoding-queries">
#
&lt;/a>
Encoding Queries
&lt;/div>
&lt;/h2>
&lt;p>The &lt;code>useContactQuery&lt;/code> hook can be simplified to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> { data, error } &lt;span style="color:#ff6ac1">=&lt;/span> useQuery(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .select(&lt;span style="color:#5af78e">`id,username,ticket_number`&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .eq(&lt;span style="color:#5af78e">&amp;#39;id&amp;#39;&lt;/span>, contactId)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .single()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Thats it. Supabase Cache Helpers will encode the client object into a definite cache key, and pass it to a generic fetcher function that makes the request. The key itself will contain all important information about the query:&lt;/p>
&lt;ul>
&lt;li>schema and table name&lt;/li>
&lt;li>selected columns&lt;/li>
&lt;li>applied filters&lt;/li>
&lt;li>body of the request (for encoding &lt;code>.rpc()&lt;/code> queries)&lt;/li>
&lt;li>count settings&lt;/li>
&lt;li>whether it is a head query&lt;/li>
&lt;li>what ordering is applied&lt;/li>
&lt;/ul>
&lt;p>For the (simple) query above, the query key in SWR is:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>;[
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;postgrest&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// key prefix
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;null&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// for swr, this is set to a constant if its an infinite query
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;public&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// schema
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// table
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;select=id%2Cusername%2Cticket_number&amp;amp;username=eq.psteinroe&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// sorted search params including selected columns and filters
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;null&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// body (for encoding `.rpc()` queries)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;count=exact&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// count settings
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;head=false&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// whether it is a head query
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;&amp;#39;&lt;/span>, &lt;span style="color:#78787e">// no ordering
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>]
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This key contains all information of the query it belongs to, and will help us to automatically populate the cache after mutations, or when subscription data is arriving. All information is extracted from the &lt;code>PostgrestClient&lt;/code> instance. Internally, the client simply sets a few protected properties, and builds an &lt;code>URL&lt;/code> object. Luckily, only TypeScript knows what a protected property is, so we can access them via &lt;code>client['url']&lt;/code> and extract the query details. For pagination and infinite scroll query keys, the encoding works the same.&lt;/p>
&lt;h2 id="mutating-data" >
&lt;div>
&lt;a href="#mutating-data">
#
&lt;/a>
Mutating Data
&lt;/div>
&lt;/h2>
&lt;p>Similar to queries, we can replace the mutation fetcher and hook with a one-liner:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> { trigger } &lt;span style="color:#ff6ac1">=&lt;/span> useUpsertMutation(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> [&lt;span style="color:#5af78e">&amp;#39;id&amp;#39;&lt;/span>], &lt;span style="color:#78787e">// primary keys of `contact`
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#5af78e">&amp;#39;id,username&amp;#39;&lt;/span> &lt;span style="color:#78787e">// will be passed to .select(`id,username`)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>To auto-populate the data the same way the custom code above does, knowledge about the required paths, and applied filters of the query is leveraged to ensure that the mutated data is still a valid member of the query. For example, a contact should only be inserted into the cache of the following query, if its ticket number is less than 100, and if it defines values for &lt;code>id&lt;/code> and &lt;code>username&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> { data, error } &lt;span style="color:#ff6ac1">=&lt;/span> useQuery(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> client.&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>).select(&lt;span style="color:#5af78e">`id,username`&lt;/span>).lt(&lt;span style="color:#5af78e">&amp;#39;ticket_number&amp;#39;&lt;/span>, &lt;span style="color:#ff9f43">100&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note that the exemplary upsert mutation above only selects &lt;code>id&lt;/code> and &lt;code>username&lt;/code>, and there is no way to know whether it would be a valid member of the query that filters on &lt;code>ticket_number&lt;/code>. To ensure that every mutation returns all data required to make relevant updates to the cache, cache helpers, &lt;strong>by default&lt;/strong>, auto-expands the query. Before the request is being made, all cache keys are scanned for queries on the same table, and the &lt;code>.select()&lt;/code> statement is expanded to include all paths that any query selects or filters on. Note that this works for any valid PostgREST query, no matter the complexity. For the example above, the actual select statement of the request will be &lt;code>id,username,ticket_number&lt;/code>. Since this can have a performance impact, you can opt-out of this feature on a per-mutation level.&lt;/p>
&lt;p>After the mutation request returns the data, the entire query cache is searched for keys that target the same schema and table as the mutation itself. For each key, the input is checked against a set of conditions that depend on the type of operation to decide whether to mutate the cache for that key. During mutation, all relevant information about the query is observed, including &lt;code>.limit()&lt;/code> filters and &lt;code>.order()&lt;/code> modifiers. If you have a paginated table with contacts ordered by &lt;code>username&lt;/code>, the new contact will be inserted in the same position as it is when the query is fetched from the server.&lt;/p>
&lt;p>The fine-grained cache update is enabled by &lt;code>PostgrestFilter&lt;/code>, which takes the parsed query key and creates filter functions to evaluate whether an object is valid given the query filters (&lt;code>applyFilters&lt;/code>) and select statements (&lt;code>hasPaths&lt;/code>). For the above example, &lt;code>hasPaths&lt;/code> returns &lt;code>true&lt;/code> if the input object returned by the mutation defines values for &lt;code>id&lt;/code>, &lt;code>username&lt;/code>, and &lt;code>ticket_number&lt;/code>. For &lt;code>applyFilters&lt;/code> to return &lt;code>true&lt;/code>, the input object must define a &lt;code>ticket_number&lt;/code> which is less than 100. If the input has no value for the path that the query filters for, &lt;code>false&lt;/code> is returned. Note that both methods work with queries of any complexity:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-tsx" data-lang="tsx">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">const&lt;/span> filter &lt;span style="color:#ff6ac1">=&lt;/span> PostgrestFilter.fromFilterBuilder(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> supabase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">from&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;contact&amp;#39;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .select(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#39;id,username,ticket_number,golden_ticket,tags,country!inner(code,name,full_name)&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> )
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .or(&lt;span style="color:#5af78e">`username.eq.unknown,and(ticket_number.eq.2,golden_ticket.is.true)`&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">is&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;golden_ticket&amp;#39;&lt;/span>, &lt;span style="color:#ff6ac1">true&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .&lt;span style="color:#ff6ac1">in&lt;/span>(&lt;span style="color:#5af78e">&amp;#39;username&amp;#39;&lt;/span>, [&lt;span style="color:#5af78e">&amp;#39;thorwebdev&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .contains(&lt;span style="color:#5af78e">&amp;#39;tags&amp;#39;&lt;/span>, [&lt;span style="color:#5af78e">&amp;#39;supateam&amp;#39;&lt;/span>])
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> .or(&lt;span style="color:#5af78e">&amp;#39;name.eq.unknown,and(name.eq.Singapore,code.eq.SG)&amp;#39;&lt;/span>, {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> foreignTable&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;country&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>console.log(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> filter.apply({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;68d2e5ef-d117-4f0c-abc7-60891a643571&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> username&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;thorwebdev&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ticket_number: &lt;span style="color:#9aedfe">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> golden_ticket: &lt;span style="color:#9aedfe">false&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tags&lt;span style="color:#ff6ac1">:&lt;/span> [&lt;span style="color:#5af78e">&amp;#39;supateam&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;investor&amp;#39;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> country&lt;span style="color:#ff6ac1">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> code&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;SG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;Singapore&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> full_name&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;Republic of Singapore&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) &lt;span style="color:#78787e">// --&amp;gt; false
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span>console.log(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> filter.apply({
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> id&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;68d2e5ef-d117-4f0c-abc7-60891a643571&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> created_at&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;2022-08-19T15:30:33.072441+00:00&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> username&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;thorwebdev&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ticket_number: &lt;span style="color:#9aedfe">2&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> golden_ticket: &lt;span style="color:#9aedfe">true&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> tags&lt;span style="color:#ff6ac1">:&lt;/span> [&lt;span style="color:#5af78e">&amp;#39;supateam&amp;#39;&lt;/span>, &lt;span style="color:#5af78e">&amp;#39;investor&amp;#39;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> country&lt;span style="color:#ff6ac1">:&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> code&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;SG&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> name&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;Singapore&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> full_name&lt;span style="color:#ff6ac1">:&lt;/span> &lt;span style="color:#5af78e">&amp;#39;Republic of Singapore&amp;#39;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> })
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>) &lt;span style="color:#78787e">// --&amp;gt; true
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Internally, &lt;code>PostgrestFilter&lt;/code> implements all operators supported by PostgREST in JavaScript. Undoubtedly, it will not be able to perfectly replicate the behavior of Postgres. However, it seems to be good enough for this use case and we have been using the Cache Helpers successfully in production for months now.&lt;/p>
&lt;h2 id="whats-next" >
&lt;div>
&lt;a href="#whats-next">
#
&lt;/a>
What&amp;rsquo;s next
&lt;/div>
&lt;/h2>
&lt;p>At the moment, only SWR and React Query are supported. However, all the logic is split into framework-independent TypeScript packages, and I&amp;rsquo;d love to add adapters for other libraries and frameworks. If you are interested in collaborating, please &lt;a href="https://github.com/psteinroe/supabase-cache-helpers/issues">file an issue&lt;/a> or contact me directly!&lt;/p></description></item><item><title>Set up a monorepo with Supabase and Turborepo</title><link>/posts/supabase-turborepo/</link><pubDate>Tue, 06 Dec 2022 00:00:00 +0000</pubDate><author>philipp@steinroetter.com (psteinroe)</author><guid>/posts/supabase-turborepo/</guid><description>&lt;p>At my current company, we recently migrated our codebase to a &lt;a href="https://turbo.build/repo">turborepo&lt;/a>-powered &lt;a href="https://pnpm.io">pnpm&lt;/a> monorepo, and are now shipping three NextJS apps, a Preact widget, a React Native app and two Fastify servers out of a single repository, all speaking to the same Supabase instance. A monorepo setup not only allows us to share code and configurations, but also decreases ci time and thereby saves $$. It took me a few attempts to properly integrate Supabase with Turborepo, and here is a quick rundown of what I&amp;rsquo;ve learned setting up the monorepo.&lt;/p>
&lt;p>&lt;strong>tl;dr;&lt;/strong> check out the &lt;a href="https://github.com/psteinroe/supasample">supasample repository&lt;/a> if you prefer to read code.&lt;/p>
&lt;p>This guide assumes you have basic knowledge about Turborepo and setup a project using pnpm and turborepo already. If that is not the case, I highly recommend skimming through &lt;a href="https://turbo.build/repo/docs/getting-started/create-new">the turborepo documentation&lt;/a>. It is one the best I have ever read.&lt;/p>
&lt;h2 id="declare-supabase-as-a-workspace" >
&lt;div>
&lt;a href="#declare-supabase-as-a-workspace">
#
&lt;/a>
Declare Supabase as a Workspace
&lt;/div>
&lt;/h2>
&lt;p>&lt;a href="https://turbo.build/repo/docs/handbook/workspaces">Workspaces&lt;/a> are the building blocks of every monorepo, and are managed by the package manager. To properly integrate Supabase into the monorepo, the &lt;code>supabase/&lt;/code> directory has to be declared as a workspace itself. First, create &lt;code>supabase/package.json&lt;/code> and give the workspace a name. I like to prefix all local workspaces with the organisation name, but you can choose any name you want.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;@supasample/supabase&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next, to make pnpm aware of the &lt;code>@supasample/supabase&lt;/code> workspace, add its path to the &lt;code>pnpm-workspace.yaml&lt;/code> file.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ff6ac1">packages&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#5af78e">&amp;#39;apps/*&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#5af78e">&amp;#39;packages/*&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#5af78e">&amp;#39;supabase&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="add-supabase-cli-commands-to-the-supabase-workspace" >
&lt;div>
&lt;a href="#add-supabase-cli-commands-to-the-supabase-workspace">
#
&lt;/a>
Add Supabase CLI Commands to the Supabase Workspace
&lt;/div>
&lt;/h2>
&lt;p>To integrate Supabase in the default development workflow, you need to add the common CLI commands to &lt;code>supabase/package.json&lt;/code>, namely &lt;code>start&lt;/code>, &lt;code>db reset&lt;/code>, &lt;code>db test&lt;/code>, &lt;code>db lint&lt;/code> and &lt;code>deploy&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;name&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;@supasample/supabase&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;scripts&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;start&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;supabase status || supabase start&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;reset&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;supabase db reset || supabase start&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;test&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;supabase db test&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;lint&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;supabase db lint&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;deploy&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;supabase link --project-ref $SUPABASE_PROJECT_REF &amp;amp;&amp;amp; supabase db push&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note that both &lt;code>start&lt;/code> and &lt;code>reset&lt;/code> use a try-catch mechanism to make sure that the desired result is achieved, no matter in what state your local Supabase instance currently is.&lt;/p>
&lt;p>Next, all scripts declared within the Supabase workspace have to be configured in &lt;code>turbo.json&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;$schema&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;https://turborepo.org/schema.json&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;pipeline&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">&amp;#34;test&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;^build&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;coverage/**&amp;#34;&lt;/span>]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;start&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;lint&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;format:check&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;deploy&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;globalEnv&amp;#34;&lt;/span>: [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;NODE_ENV&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;SUPABASE_URL&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;SUPABASE_ANON_KEY&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#5af78e">&amp;#34;SUPABASE_SERVICE_KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ]
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Note that the snippet contains only the few configurations necessary for the supabase workspace, you will most likely have a few more in yours.&lt;/p>
&lt;p>To test that everything works, run &lt;code>pnpm run start --filter=supabase&lt;/code> from the repository root. Make sure that your local Docker host is up.&lt;/p>
&lt;h2 id="configure-workspace-task-to-workspace-task-dependencies" >
&lt;div>
&lt;a href="#configure-workspace-task-to-workspace-task-dependencies">
#
&lt;/a>
Configure workspace-task to workspace-task dependencies
&lt;/div>
&lt;/h2>
&lt;p>Withe the current configuration, you have to run &lt;code>pnpm run start --filter=supabase&lt;/code> or &lt;code>supabase start&lt;/code> expliclity before executing any task that requires a running Supabase instance. A GitHub Action workflow might look like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ff6ac1">name&lt;/span>: 🏗 Setup Supabase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">uses&lt;/span>: supabase/setup-cli@v1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">with&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">version&lt;/span>: latest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ff6ac1">name&lt;/span>: 🚀 Start Supabase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">run&lt;/span>: supabase db start
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ff6ac1">name&lt;/span>: 🦺 Check Format, Lint, Typecheck and Test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">run&lt;/span>: pnpm run turbo format:check lint:report typecheck test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Although the team is making continous improvements, starting Supabase takes around 60-90 seconds of your costly GitHub Actions time. In most projects, your database will change the least, and the &lt;code>test&lt;/code> and &lt;code>lint&lt;/code> tasks will be replayed from the Turborepo cache 90% of the time. Hence, you are throwing away money here. Fortunately, Turborepo supports &lt;a href="https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks#specific-workspace-tasks">workspace-task to workspace-task dependencies&lt;/a>, that can be expressed using the &lt;code>&amp;lt;workspace&amp;gt;#&amp;lt;task&amp;gt;&lt;/code> syntax. You can leverage this feature to have Turobrepo decide whether it is required to start Supabase.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;pipeline&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">&amp;#34;@supasample/supabase#start&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: [],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;cache&amp;#34;&lt;/span>: &lt;span style="color:#ff6ac1">false&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;@supasample/supabase#test&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;@supasample/supabase#start&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;^build&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;@supasample/supabase#lint&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;@supasample/supabase#start&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;format:check&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Although it might appear counter intuitive, you want to disable caching for &lt;code>supabase start&lt;/code>. Next, to ensure that the supabase instance is running before the database is tested or linted, you the respective task configurations, and add the &lt;code>start&lt;/code> script as a dependency.&lt;/p>
&lt;p>If you now run &lt;code>pnpm run test --filter=supabase&lt;/code>, the supabase instance will be started if not already up, and your GitHub Action can be simplified to&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ff6ac1">name&lt;/span>: 🏗 Setup Supabase
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">uses&lt;/span>: supabase/setup-cli@v1
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">with&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">version&lt;/span>: latest
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ff6ac1">name&lt;/span>: 🦺 Check Format, Lint, Typecheck and Test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">run&lt;/span>: pnpm run turbo format:check lint:report typecheck test
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ...
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="add-types-package" >
&lt;div>
&lt;a href="#add-types-package">
#
&lt;/a>
Add &lt;code>types&lt;/code> Package
&lt;/div>
&lt;/h2>
&lt;p>The same configuration can be used for other workspaces too. For example, you might have a shared &lt;code>types&lt;/code> package in your monorepo, from which all packages and apps import the database types generated by the Supabase CLI.&lt;/p>
&lt;p>First, add a &lt;code>generate&lt;/code> script to the respective &lt;code>package.json&lt;/code>.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;scripts&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">&amp;#34;generate&amp;#34;&lt;/span>: &lt;span style="color:#5af78e">&amp;#34;(cd ../../ &amp;amp;&amp;amp; supabase gen types typescript --local &amp;gt; ./packages/types/src/database.ts) &amp;amp;&amp;amp; prettier --write \&amp;#34;src/**/*.{ts,tsx,md}\&amp;#34;&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The script first changes into the root directory before running the type generation. All types are written into a &lt;code>database.ts&lt;/code> file of the same package. Finally, the generated types are formatted using prettier.&lt;/p>
&lt;p>You might have noticed that the &lt;code>--local&lt;/code> flag is used. To ensure you have a Supabase instance running, configure the &lt;code>generate&lt;/code> task similar to the &lt;code>test&lt;/code> and &lt;code>lint&lt;/code> tasks within the &lt;code>turbo.json&lt;/code> file:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;pipeline&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> &lt;span style="color:#ff6ac1">&amp;#34;generate&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;^generate&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> },
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;@supasample/types#generate&amp;#34;&lt;/span>: {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;dependsOn&amp;#34;&lt;/span>: [&lt;span style="color:#5af78e">&amp;#34;@supasample/supabase#start&amp;#34;&lt;/span>, &lt;span style="color:#5af78e">&amp;#34;^generate&amp;#34;&lt;/span>],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#ff6ac1">&amp;#34;outputs&amp;#34;&lt;/span>: []
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#78787e">// ...
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#78787e">&lt;/span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If you now run &lt;code>pnpm run generate&lt;/code>, and Turborepo will make sure you have a Supabase instance running before the types are generated.&lt;/p>
&lt;p>Thanks for reading!&lt;/p></description></item></channel></rss>