<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Mastering Postgres &amp; Supabase: Mansueli Tips for Seamless Data Optimization]]></title><description><![CDATA[Explore our in-depth articles on Postgres &amp; Supabase. Learn valuable tips and techniques to enhance data management and optimization.]]></description><link>https://blog.mansueli.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 10 Apr 2026 12:24:19 GMT</lastBuildDate><atom:link href="https://blog.mansueli.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Secure Your Supabase Auth with email_guard]]></title><description><![CDATA[Building a secure authentication system is not just about strong passwords. It starts with checking the quality of user signups in your Supabase.com project. Disposable email addresses often lead to spam accounts, abuse, and fake users that fill up y...]]></description><link>https://blog.mansueli.com/secure-your-supabase-auth-with-emailguard</link><guid isPermaLink="true">https://blog.mansueli.com/secure-your-supabase-auth-with-emailguard</guid><category><![CDATA[supabase]]></category><category><![CDATA[supabase auth]]></category><category><![CDATA[Security]]></category><category><![CDATA[accounts]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Thu, 30 Oct 2025 16:34:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761842000267/004cc382-85fd-4b4a-baa0-597677c0201f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Building a secure authentication system is not just about strong passwords. It starts with checking the quality of user signups in your <a target="_blank" href="http://supabase.com/">Supabase.com</a> project. Disposable email addresses often lead to spam accounts, abuse, and fake users that fill up your database. Also, Gmail's flexible rules (like dots and <code>+tags</code>) let people make many accounts from one email.</p>
<p>Meet <strong>email_guard</strong>, a Trusted Language Extension (TLE) for PostgreSQL. It works well with Supabase Auth hooks to:</p>
<ul>
<li><strong>Block disposable email domains</strong> with a big, auto-updated list.</li>
<li><strong>Normalize Gmail addresses</strong> to stop duplicate signups (by removing dots and <code>+tags</code>).</li>
<li><strong>Offer easy installation</strong> that fits in any schema.</li>
</ul>
<p>When you use this with Supabase's own security tools, like <a target="_blank" href="https://supabase.com/docs/guides/auth/passwords">leaked password protection</a>, you build strong protection against bad signups and abuse.</p>
<h2 id="heading-why-you-need-emailguard-for-supabase-authentication-security">Why You Need email_guard for Supabase Authentication Security</h2>
<h3 id="heading-the-problem-disposable-emails-and-gmail-tricks">The Problem: Disposable Emails and Gmail Tricks</h3>
<p><strong>Disposable email services</strong> (like mailinator.com or guerrillamail.com) let anyone make short-term emails. These are good for tests, but often used to:</p>
<ul>
<li>Make spam or bot accounts.</li>
<li>Skip limits on trials or rates.</li>
<li>Avoid bans by making new accounts fast.</li>
</ul>
<p><strong>Gmail addressing quirks</strong> are tricky too. These all go to the same inbox:</p>
<ul>
<li><code>john.doe@gmail.com</code></li>
<li><code>j.o.h.n.d.o.e@gmail.com</code></li>
<li><code>johndoe+promo@gmail.com</code></li>
<li><code>johndoe+whatever@gmail.com</code></li>
</ul>
<p>Without fixing this, a user can make many accounts by adding dots or <code>+tags</code>. This breaks rules for one account per person and can cheat referral programs or trials.</p>
<h3 id="heading-the-solution-smart-email-validation-at-signup-in-supabase">The Solution: Smart Email Validation at Signup in Supabase</h3>
<p>The <code>email_guard</code> TLE gives you:</p>
<ol>
<li><strong>A blocklist for disposable domains</strong> with over 20,000 known ones (updated weekly).</li>
<li><strong>Gmail normalization</strong> that removes dots and <code>+tags</code>, and sets the domain to <code>gmail.com</code>.</li>
<li><strong>A helper for Supabase Auth hooks</strong> that checks before creating a user.</li>
</ol>
<p>All this runs in PostgreSQL, so it is fast, safe, and clear. For more on Supabase auth, see the <a target="_blank" href="https://supabase.com/docs/guides/auth">Supabase Auth docs</a>.</p>
<hr />
<h2 id="heading-understanding-trusted-language-extensions-tle-in-supabase">Understanding Trusted Language Extensions (TLE) in Supabase</h2>
<p>Before installation, let's quickly explain what a Trusted Language Extension is and why it helps.</p>
<h3 id="heading-what-is-a-tle">What is a TLE?</h3>
<p>A <strong>Trusted Language Extension (TLE)</strong> is a PostgreSQL add-on written in a safe language (like PL/pgSQL or PL/Python). You can install it without special admin rights. This is key for hosted setups like Supabase, where you lack full access.</p>
<p>TLEs come from <a target="_blank" href="https://database.dev">database.dev</a>, the package manager for PostgreSQL. This makes them:</p>
<ul>
<li><strong>Easy to install</strong> with the Supabase CLI.</li>
<li><strong>Version-controlled</strong> for safe updates.</li>
<li><strong>Flexible</strong> to place in any schema.</li>
<li><strong>Safe for production</strong>.</li>
</ul>
<p>Learn more in Supabase's <a target="_blank" href="https://supabase.com/blog/pg-tle">Trusted Language Extensions for Postgres blog post</a> or the <a target="_blank" href="https://supabase.com/docs/guides/database/extensions">extensions docs</a>.</p>
<h3 id="heading-why-use-tles-for-security-in-supabase">Why Use TLEs for Security in Supabase?</h3>
<p>With security logic in a TLE:</p>
<ul>
<li><strong>It runs in the database</strong>, near your data.</li>
<li><strong>It stays the same</strong> for all apps and calls.</li>
<li><strong>You can check it</strong> with version history.</li>
<li><strong>It cannot be skipped</strong> by app code.</li>
</ul>
<p>This is great for auth checks, data rules, and policies.</p>
<hr />
<h2 id="heading-step-1-installing-the-tle-infrastructure-on-supabase">Step 1: Installing the TLE Infrastructure on Supabase</h2>
<p>Before adding <code>email_guard</code>, set up the <code>pg_tle</code> extension (already on Supabase) and the <code>dbdev</code> tool for easy install.</p>
<h3 id="heading-a-set-up-dbdev-and-supabase-cli-if-needed">A. Set Up dbdev and Supabase CLI (If Needed)</h3>
<p>If not done yet:</p>
<ol>
<li><strong>Install dbdev CLI</strong>: Follow the <a target="_blank" href="https://supabase.github.io/dbdev/getting-started/">dbdev getting started guide</a>.</li>
<li><strong>Install Supabase CLI</strong>: Check the <a target="_blank" href="https://supabase.com/docs/guides/cli">Supabase CLI docs</a>.</li>
<li><strong>Link your project</strong>: Run <code>supabase link</code> to connect to your Supabase database.</li>
</ol>
<p>Supabase has <code>pg_tle</code> ready, so create it:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> pg_tle;
</code></pre>
<h3 id="heading-b-generate-the-migration-with-dbdev">B. Generate the Migration with dbdev</h3>
<p>Use dbdev to get the latest <code>email_guard</code> (version 0.3.1 or newer) in your migrations:</p>
<pre><code class="lang-bash">dbdev add \
  -o ./supabase/migrations/ \
  -v 0.3.1 \
  -s extensions \
  package \
  -n mansueli@email_guard
</code></pre>
<p><strong>What this does:</strong></p>
<ul>
<li>Makes a new migration file in <code>./supabase/migrations/</code>.</li>
<li>Puts the extension in the <code>extensions</code> schema (good for Supabase).</li>
<li>Uses version 0.3.1 (change for new versions).</li>
</ul>
<p>Adjust the <code>-o</code> path if your folder is different.</p>
<h3 id="heading-c-apply-the-migration">C. Apply the Migration</h3>
<p>Push to your Supabase database:</p>
<pre><code class="lang-bash">supabase db push
</code></pre>
<p>Done! The extension is installed with the full blocklist.</p>
<p>For more on managing extensions, see <a target="_blank" href="https://supabase.com/docs/guides/database/extensions">Supabase database extensions docs</a>.</p>
<hr />
<h2 id="heading-step-2-understanding-what-you-just-installed">Step 2: Understanding What You Just Installed</h2>
<p>The <code>email_guard</code> extension adds objects in your schema (like <code>extensions</code>):</p>
<h3 id="heading-table-disposableemaildomains">Table: disposable_email_domains</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Holds over 20,000 disposable email domains</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> extensions.disposable_email_domains (
  <span class="hljs-keyword">domain</span> <span class="hljs-built_in">text</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  <span class="hljs-keyword">CONSTRAINT</span> disposable_email_domains_domain_lowercase
    <span class="hljs-keyword">CHECK</span> (<span class="hljs-keyword">domain</span> = <span class="hljs-keyword">lower</span>(<span class="hljs-keyword">domain</span>))
);
</code></pre>
<p>It fills with domains like:</p>
<ul>
<li><code>mailinator.com</code></li>
<li><code>guerrillamail.com</code></li>
<li><code>10minutemail.com</code></li>
<li>And many more.</li>
</ul>
<h3 id="heading-function-normalizeemailtext">Function: normalize_email(text)</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Example: normalize_email('J.o.h.n.Doe+promo@gmail.com')</span>
<span class="hljs-comment">-- Returns: 'johndoe@gmail.com'</span>
<span class="hljs-keyword">SELECT</span> extensions.normalize_email(<span class="hljs-string">'J.o.h.n.Doe+promo@gmail.com'</span>);
</code></pre>
<p><strong>What it does:</strong></p>
<ul>
<li>Makes lowercase.</li>
<li>For Gmail/Googlemail:<ul>
<li>Removes dots from the start.</li>
<li>Cuts after <code>+</code> (and the <code>+</code>).</li>
<li>Sets domain to <code>gmail.com</code>.</li>
</ul>
</li>
<li>For others: Just lowercase.</li>
</ul>
<h3 id="heading-function-isdisposableemaildomaintext">Function: is_disposable_email_domain(text)</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Check if a domain is disposable</span>
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email_domain(<span class="hljs-string">'mailinator.com'</span>); <span class="hljs-comment">-- true</span>
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email_domain(<span class="hljs-string">'gmail.com'</span>);      <span class="hljs-comment">-- false</span>
</code></pre>
<p><strong>Smart check:</strong></p>
<ul>
<li>Looks at parent domains (e.g., <code>sub.mailinator.com</code> matches).</li>
<li>Fast with index.</li>
</ul>
<h3 id="heading-function-isdisposableemailtext">Function: is_disposable_email(text)</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Easy check for full emails</span>
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email(<span class="hljs-string">'user@guerrillamail.com'</span>); <span class="hljs-comment">-- true</span>
</code></pre>
<h3 id="heading-hook-helper-hookpreventdisposableandenforcegmailuniquenessjsonb">Hook Helper: hook_prevent_disposable_and_enforce_gmail_uniqueness(jsonb)</h3>
<p>This is the key part! For Supabase Auth hooks, it:</p>
<ol>
<li><strong>Checks disposable domains</strong> → Sends 403 error if yes.</li>
<li><strong>Normalizes Gmail</strong> → Checks if the same normalized email exists.</li>
<li><strong>Sends 409 error</strong> if duplicate.</li>
<li><strong>Allows</strong> phone signups or non-email.</li>
</ol>
<hr />
<h2 id="heading-step-3-wire-up-the-supabase-auth-hook-for-email-validation">Step 3: Wire Up the Supabase Auth Hook for Email Validation</h2>
<p>Now, connect it to signups.</p>
<h3 id="heading-navigate-to-auth-hooks-in-dashboard">Navigate to Auth Hooks in Dashboard</h3>
<ol>
<li>Go to your <a target="_blank" href="https://supabase.com/dashboard">Supabase Dashboard</a>.</li>
<li>Pick your project.</li>
<li>Go to <strong>Authentication</strong> → <strong>Hooks</strong>.</li>
<li>Turn on <strong>Before User Created</strong>.</li>
</ol>
<h3 id="heading-configure-the-hook">Configure the Hook</h3>
<p>Pick:</p>
<ul>
<li><strong>Hook Type</strong>: <code>Postgres Function</code>.</li>
<li><strong>Schema</strong>: <code>extensions</code> (or your choice).</li>
<li><strong>Function</strong>: <code>hook_prevent_disposable_and_enforce_gmail_uniqueness</code>.</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1730300100000/auth-hook-setup.png" alt="Supabase Auth Hook Configuration for email_guard" /></p>
<p>Done! The hook is on. See <a target="_blank" href="https://supabase.com/docs/guides/auth/auth-hooks">Supabase Auth Hooks docs</a> for details.</p>
<h3 id="heading-what-happens-during-signup">What Happens During Signup</h3>
<p>When signing up, the hook checks first:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Try with disposable email</span>
supabase.auth.signUp({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'test@mailinator.com'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'secure_password_123'</span>
})
<span class="hljs-comment">// ❌ Error: { message: "Disposable email addresses are not allowed", status: 403 }</span>

<span class="hljs-comment">// Try with duplicate Gmail</span>
<span class="hljs-comment">// If johndoe@gmail.com exists</span>
supabase.auth.signUp({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'j.o.h.n.d.o.e+test@gmail.com'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'secure_password_123'</span>
})
<span class="hljs-comment">// ❌ Error: { message: "A user with this normalized email already exists", status: 409 }</span>

<span class="hljs-comment">// Valid email</span>
supabase.auth.signUp({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'alice@example.com'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'secure_password_123'</span>
})
<span class="hljs-comment">// ✅ User created</span>
</code></pre>
<hr />
<h2 id="heading-step-4-testing-your-emailguard-setup-on-supabase">Step 4: Testing Your email_guard Setup on Supabase</h2>
<p>Check if it works.</p>
<h3 id="heading-test-1-check-disposable-email-detection">Test 1: Check Disposable Email Detection</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- True</span>
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email(<span class="hljs-string">'user@mailinator.com'</span>);

// False
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email(<span class="hljs-string">'user@gmail.com'</span>);
</code></pre>
<h3 id="heading-test-2-test-gmail-normalization">Test 2: Test Gmail Normalization</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- All return 'johndoe@gmail.com'</span>
<span class="hljs-keyword">SELECT</span> extensions.normalize_email(<span class="hljs-string">'John.Doe@gmail.com'</span>);
<span class="hljs-keyword">SELECT</span> extensions.normalize_email(<span class="hljs-string">'j.o.h.n.d.o.e@googlemail.com'</span>);
<span class="hljs-keyword">SELECT</span> extensions.normalize_email(<span class="hljs-string">'johndoe+promo@gmail.com'</span>);
</code></pre>
<h3 id="heading-test-3-simulate-the-hook">Test 3: Simulate the Hook</h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Disposable reject</span>
<span class="hljs-keyword">SELECT</span> extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(
  <span class="hljs-string">'{"user": {"email": "test@guerrillamail.com"}}'</span>::jsonb
);
<span class="hljs-comment">-- Error: "Disposable email addresses are not allowed"</span>

// Gmail duplicate (after adding test user)
<span class="hljs-keyword">SELECT</span> extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(
  <span class="hljs-string">'{"user": {"email": "j.o.h.n.d.o.e+test@gmail.com"}}'</span>::jsonb
);
<span class="hljs-comment">-- Error: "A user with this normalized email already exists"</span>
</code></pre>
<hr />
<h2 id="heading-step-5-combining-with-supabases-built-in-protections">Step 5: Combining with Supabase's Built-in Protections</h2>
<p><code>email_guard</code> is stronger with Supabase features.</p>
<h3 id="heading-leaked-password-protection">Leaked Password Protection</h3>
<p>Supabase checks against <a target="_blank" href="https://haveibeenpwned.com/">HaveIBeenPwned</a>. Turn it on in <strong>Dashboard → Authentication → Password Protection</strong>.</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Leaked password</span>
supabase.auth.signUp({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'alice@example.com'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'password123'</span>  <span class="hljs-comment">// Leaked</span>
})
<span class="hljs-comment">// ❌ Error: { message: "Password has been leaked", status: 422 }</span>
</code></pre>
<hr />
<h2 id="heading-keeping-the-blocklist-current-in-emailguard">Keeping the Blocklist Current in email_guard</h2>
<p><code>email_guard</code> updates itself.</p>
<h3 id="heading-how-updates-work">How Updates Work</h3>
<p>A GitHub workflow:</p>
<ol>
<li><strong>Runs every week</strong> (Mondays).</li>
<li><strong>Gets new list</strong> from <a target="_blank" href="https://github.com/disposable-email-domains/disposable-email-domains">disposable-email-domains repo</a>.</li>
<li><strong>Makes upgrade script</strong> if changed.</li>
<li><strong>Updates version</strong> (e.g., 0.3.1 to 0.3.2).</li>
<li><strong>Saves changes</strong> auto.</li>
</ol>
<h3 id="heading-upgrading-to-the-latest-version">Upgrading to the Latest Version</h3>
<p>For new version:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Make upgrade migration</span>
dbdev add \
  -o ./supabase/migrations/ \
  -v 0.3.2 \  <span class="hljs-comment"># New</span>
  -s extensions \
  package \
  -n mansueli@email_guard

<span class="hljs-comment"># Apply</span>
supabase db push
</code></pre>
<p>It adds new domains and keeps data. Watch releases on <a target="_blank" href="https://github.com/mansueli/tle/tree/master/email_guard">GitHub repo</a>.</p>
<hr />
<h2 id="heading-advanced-usage-amp-customization-for-supabase-emailguard">Advanced Usage &amp; Customization for Supabase email_guard</h2>
<h3 id="heading-custom-domain-blocking">Custom Domain Blocking</h3>
<p>Block extra domains:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Add one</span>
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> extensions.disposable_email_domains (<span class="hljs-keyword">domain</span>)
<span class="hljs-keyword">VALUES</span> (<span class="hljs-string">'suspicious-domain.com'</span>)
<span class="hljs-keyword">ON</span> CONFLICT <span class="hljs-keyword">DO</span> <span class="hljs-keyword">NOTHING</span>;

<span class="hljs-comment">-- Remove one</span>
<span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> extensions.disposable_email_domains
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">domain</span> = <span class="hljs-string">'some-domain.com'</span>;
</code></pre>
<h3 id="heading-checking-existing-users">Checking Existing Users</h3>
<p>Audit users:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Find disposable</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>, email
<span class="hljs-keyword">FROM</span> auth.users
<span class="hljs-keyword">WHERE</span> extensions.is_disposable_email(email);

<span class="hljs-comment">-- Find Gmail duplicates</span>
<span class="hljs-keyword">WITH</span> normalized <span class="hljs-keyword">AS</span> (
  <span class="hljs-keyword">SELECT</span> 
    <span class="hljs-keyword">id</span>,
    email,
    extensions.normalize_email(email) <span class="hljs-keyword">AS</span> normalized_email
  <span class="hljs-keyword">FROM</span> auth.users
  <span class="hljs-keyword">WHERE</span> email <span class="hljs-keyword">ILIKE</span> <span class="hljs-string">'%@gmail.com'</span>
     <span class="hljs-keyword">OR</span> email <span class="hljs-keyword">ILIKE</span> <span class="hljs-string">'%@googlemail.com'</span>
)
<span class="hljs-keyword">SELECT</span> 
  normalized_email,
  array_agg(email) <span class="hljs-keyword">AS</span> duplicate_emails,
  <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">AS</span> duplicate_count
<span class="hljs-keyword">FROM</span> normalized
<span class="hljs-keyword">GROUP</span> <span class="hljs-keyword">BY</span> normalized_email
<span class="hljs-keyword">HAVING</span> <span class="hljs-keyword">count</span>(*) &gt; <span class="hljs-number">1</span>;
</code></pre>
<h3 id="heading-custom-hook-logic">Custom Hook Logic</h3>
<p>Make your own hook:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> public.my_custom_signup_hook(<span class="hljs-keyword">event</span> jsonb)
<span class="hljs-keyword">RETURNS</span> jsonb
<span class="hljs-keyword">LANGUAGE</span> plpgsql
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  user_email <span class="hljs-built_in">text</span>;
<span class="hljs-keyword">BEGIN</span>
  user_email := <span class="hljs-keyword">event</span>-&gt;<span class="hljs-string">'user'</span>-&gt;&gt;<span class="hljs-string">'email'</span>;

  IF extensions.is_disposable_email(user_email) THEN
    RAISE EXCEPTION 'Nice try! No disposable emails here.'
      USING HINT = 'Please <span class="hljs-keyword">use</span> a <span class="hljs-keyword">permanent</span> email address<span class="hljs-string">',
            ERRCODE = '</span>P0001<span class="hljs-string">';
  END IF;

  -- Add custom rules

  RETURN event;
END;
$$;</span>
</code></pre>
<hr />
<h2 id="heading-performance-considerations-for-emailguard-in-supabase">Performance Considerations for email_guard in Supabase</h2>
<h3 id="heading-benchmarking">Benchmarking</h3>
<p>Functions are fast:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Domain check: ~0.1ms</span>
<span class="hljs-keyword">SELECT</span> extensions.is_disposable_email_domain(<span class="hljs-string">'mailinator.com'</span>);

<span class="hljs-comment">-- Normalize: ~0.05ms</span>
<span class="hljs-keyword">SELECT</span> extensions.normalize_email(<span class="hljs-string">'j.o.h.n.d.o.e+test@gmail.com'</span>);

<span class="hljs-comment">-- Full hook: ~1-2ms</span>
</code></pre>
<h3 id="heading-index-optimization">Index Optimization</h3>
<p>Extension adds index on <code>auth.users(email)</code>. For big databases:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Partial index for Gmail</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> users_gmail_normalized_idx 
<span class="hljs-keyword">ON</span> auth.users (extensions.normalize_email(email))
<span class="hljs-keyword">WHERE</span> email <span class="hljs-keyword">ILIKE</span> <span class="hljs-string">'%@gmail.com'</span> <span class="hljs-keyword">OR</span> email <span class="hljs-keyword">ILIKE</span> <span class="hljs-string">'%@googlemail.com'</span>;
</code></pre>
<hr />
<h2 id="heading-troubleshooting-emailguard-on-supabase">Troubleshooting email_guard on Supabase</h2>
<h3 id="heading-hook-not-triggering">Hook Not Triggering</h3>
<p><strong>Check setup:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> supabase_functions.hooks
<span class="hljs-keyword">WHERE</span> hook_name = <span class="hljs-string">'before_user_created'</span>;
</code></pre>
<p><strong>Check rights:</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> extensions.hook_prevent_disposable_and_enforce_gmail_uniqueness(jsonb)
<span class="hljs-keyword">TO</span> supabase_auth_admin;
</code></pre>
<h3 id="heading-false-positives">False Positives</h3>
<p>If wrong block:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> extensions.disposable_email_domains
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">domain</span> = <span class="hljs-string">'legitimate-domain.com'</span>;
</code></pre>
<p>Report to <a target="_blank" href="https://github.com/disposable-email-domains/disposable-email-domains">blocklist repo</a>.</p>
<h3 id="heading-migration-conflicts">Migration Conflicts</h3>
<p>Use different schema:</p>
<pre><code class="lang-bash">dbdev add \
  -o ./supabase/migrations/ \
  -v 0.3.1 \
  -s email_guard \
  package \
  -n mansueli@email_guard
</code></pre>
<p>Update hook to <code>email_guard.hook_prevent_disposable_and_enforce_gmail_uniqueness</code>.</p>
<hr />
<h2 id="heading-security-best-practices-for-supabase-authentication">Security Best Practices for Supabase Authentication</h2>
<h3 id="heading-defense-in-depth">Defense in Depth</h3>
<p>Add layers:</p>
<ol>
<li><strong>Verify emails</strong>: Make users confirm.</li>
<li><strong>CAPTCHA</strong>: Use hCaptcha on forms.</li>
<li><strong>Rate limits</strong>: Stop many tries per IP.</li>
<li><strong>Review accounts</strong>: Flag odd patterns.</li>
</ol>
<h3 id="heading-monitoring">Monitoring</h3>
<p>Track blocks:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> blocked_signups (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid() PRIMARY <span class="hljs-keyword">KEY</span>,
  email <span class="hljs-built_in">text</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  reason <span class="hljs-built_in">text</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  created_at timestamptz <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">now</span>()
);

<span class="hljs-comment">-- Log in hook (add yourself)</span>
</code></pre>
<h3 id="heading-privacy-considerations">Privacy Considerations</h3>
<ul>
<li><strong>Avoid logging full emails</strong>.</li>
<li><strong>Hash data</strong> for stats.</li>
<li><strong>Follow GDPR/CCPA</strong> for stored info.</li>
</ul>
<hr />
<h2 id="heading-conclusion-building-a-secure-foundation-with-emailguard-on-supabase">Conclusion: Building a Secure Foundation with email_guard on Supabase</h2>
<p>Authentication is the door to your app. Secure it with layers. The <code>email_guard</code> TLE gives a simple way to block disposable emails and stop Gmail duplicates in Supabase.</p>
<p>With Supabase tools like leaked password checks, email verification, and rate limits, you get a strong system that:</p>
<ul>
<li>✅ <strong>Blocks bad signups</strong> auto.</li>
<li>✅ <strong>Stops abuse</strong> without work.</li>
<li>✅ <strong>Grows with your app</strong>.</li>
<li>✅ <strong>Updates weekly</strong>.</li>
</ul>
<p>It runs in the database, so it is clear, checked, and hard to skip.</p>
<h3 id="heading-next-steps">Next Steps</h3>
<ol>
<li><strong>Install the extension</strong> as shown.</li>
<li><strong>Turn on the auth hook</strong> in dashboard.</li>
<li><strong>Test</strong> with disposable and Gmail tests.</li>
<li><strong>Watch logs</strong> for blocks.</li>
<li><strong>Update</strong> when new versions come.</li>
</ol>
<p>For more, see:</p>
<ul>
<li><a target="_blank" href="https://github.com/mansueli/tle/tree/master/email_guard">email_guard GitHub repository</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/auth-hooks">Supabase Auth Hooks documentation</a></li>
<li><a target="_blank" href="https://supabase.com/blog/pg-tle">Trusted Language Extensions blog post</a></li>
<li><a target="_blank" href="https://database.dev">database.dev package registry</a></li>
</ul>
<p>Check my previous posts: <a target="_blank" href="https://blog.mansueli.com/building-user-authentication-with-username-and-password-using-supabase">Building User Authentication with Username and Password Using Supabase</a> and <a target="_blank" href="https://blog.mansueli.com/streamlining-postgresql-function-management-with-supabase">Streamlining PostgreSQL Function Management with Supabase</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Building Secure API Key Management with Supabase, KSUID & PostgreSQL]]></title><description><![CDATA[Managing API keys seems easy at first, but it involves many choices. You need keys that are secure, fast to check, easy to use, and simple to sort. This guide shows how to build secure API key management with Supabase.com. We use PostgreSQL and KSUID...]]></description><link>https://blog.mansueli.com/building-secure-api-key-management-with-supabase-ksuid-and-postgresql</link><guid isPermaLink="true">https://blog.mansueli.com/building-secure-api-key-management-with-supabase-ksuid-and-postgresql</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[APIs]]></category><category><![CDATA[api key]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Wed, 20 Aug 2025 11:41:38 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755689230616/7c637884-6f19-48b1-a110-790627c66002.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Managing API keys seems easy at first, but it involves many choices. You need keys that are secure, fast to check, easy to use, and simple to sort. This guide shows how to build secure API key management with <a target="_blank" href="http://supabase.com/">Supabase.com</a>. We use PostgreSQL and KSUID to meet these needs.</p>
<p>In this post, you will learn a simple pattern with Supabase, PostgreSQL, and KSUID (via the pg_idkit extension). By the end, you get SQL code to add to your database. You also get workflows to create, check, and list API keys. This setup is optimized for Supabase users, with tips on the Supabase CLI and JavaScript client. It helps with secure API key management in Supabase.</p>
<p>For more on secrets in Supabase, see previous posts on <a target="_blank" href="https://supabase.com/blog/supabase-vault">Supabase Vault</a>. If you work with PostgreSQL extensions, check out <a target="_blank" href="https://supabase.com/blog/pg-tle">Trusted Language Extensions for Postgres</a> which is used on this blog post.</p>
<hr />
<h2 id="heading-overview-a-simple-way-to-manage-api-keys">Overview: A Simple Way to Manage API Keys</h2>
<p>We make API keys that look like this:</p>
<pre><code>[KSUID][SECRET]
</code></pre><ul>
<li>The KSUID is a short ID that sorts by time. We use it as the row ID.</li>
<li>The SECRET is a random string shown once to the user.</li>
<li>In the database, we store:<ul>
<li>The KSUID (as id),</li>
<li>A salted hash of the secret,</li>
<li>A random salt,</li>
<li>The last 4 characters for the dashboard,</li>
<li>The client_id (UUID) to link to a Supabase user (see <a target="_blank" href="https://supabase.com/docs/guides/auth">Supabase Auth docs</a>),</li>
<li>And a name for the key label.</li>
</ul>
</li>
</ul>
<p>To check a key: Split it into KSUID and secret. Find the row by KSUID (fast with index). Compute the hash and compare. This is quick: one lookup and one hash.</p>
<p>This method keeps your Supabase API key management secure and efficient.</p>
<hr />
<h2 id="heading-step-1-install-pgidkit-extension-with-dbdev-and-supabase-cli">Step 1: Install pg_idkit Extension with dbdev and Supabase CLI</h2>
<p>The pg_idkit extension gives KSUID functions like gen_random_ksuid_microsecond(). We install it as a Trusted Language Extension (TLE) using dbdev CLI to generate a migration. Then, use Supabase CLI to apply it. This fits well with Supabase workflows for installing PostgreSQL extensions in Supabase.</p>
<h3 id="heading-a-set-up-dbdev-and-supabase-cli">A. Set Up dbdev and Supabase CLI</h3>
<p>If you do not have dbdev CLI, install it. Follow the <a target="_blank" href="https://supabase.github.io/dbdev/getting-started/">dbdev installation docs</a>. For Supabase CLI, see the <a target="_blank" href="https://supabase.com/docs/guides/cli">Supabase CLI docs</a>. Link your local project to your Supabase database.</p>
<p>Note: On Supabase, the pg_tle extension is already installed. This meets the prerequisite.</p>
<h3 id="heading-b-generate-migration-file-with-dbdev">B. Generate Migration File with dbdev</h3>
<p>Run this command to create a migration file for pg_idkit version 0.0.4 in the extensions schema:</p>
<pre><code class="lang-bash">dbdev add -o <span class="hljs-string">"./supabase/migrations/"</span> -v 0.0.4 -s extensions package -n kiwicopple@pg_idkit
</code></pre>
<ul>
<li>This makes a SQL file in your migrations folder.</li>
<li>Adjust the path if your migrations folder is different.</li>
</ul>
<p>For more details, see the <a target="_blank" href="https://supabase.github.io/dbdev/install-a-package/">dbdev install a package docs</a>.</p>
<h3 id="heading-c-apply-the-migration-with-supabase-cli">C. Apply the Migration with Supabase CLI</h3>
<p>Push the migration to your Supabase database:</p>
<pre><code class="lang-bash">supabase db push
</code></pre>
<p>This applies the extension. Now you can use functions like:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> gen_random_ksuid_microsecond();
</code></pre>
<p>For more on extensions, see <a target="_blank" href="https://supabase.com/docs/guides/database/extensions">Supabase Database Extensions docs</a>.</p>
<hr />
<h2 id="heading-step-2-set-up-the-table-schema">Step 2: Set Up the Table Schema</h2>
<p>Use this SQL in your Supabase SQL editor or a migration file. It creates the api_keys table with KSUID as the primary key.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> pgcrypto;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> api_keys (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">TEXT</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_ksuid_microsecond(), <span class="hljs-comment">-- KSUID as ID (27 chars)</span>
  client_id <span class="hljs-keyword">UUID</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">REFERENCES</span> auth.users(<span class="hljs-keyword">id</span>) <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,                    <span class="hljs-comment">-- user label</span>
  secret_hash BYTEA <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,            <span class="hljs-comment">-- digest(secret || salt)</span>
  <span class="hljs-keyword">salt</span> BYTEA <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> gen_random_bytes(<span class="hljs-number">16</span>), <span class="hljs-comment">-- auto salt</span>
  last4 <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,                   <span class="hljs-comment">-- last 4 chars of secret for UI</span>
  created_at TIMESTAMPTZ <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">now</span>()
);
<span class="hljs-comment">-- Index for fast per-user listing, ordered by KSUID (time-sortable)</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> api_keys_client_id_idx <span class="hljs-keyword">ON</span> api_keys (client_id, <span class="hljs-keyword">id</span> <span class="hljs-keyword">DESC</span>);
</code></pre>
<p>Notes:</p>
<ul>
<li>KSUIDs sort by time and are 27 characters long.</li>
<li>We link to auth.users for Supabase auth.</li>
<li>Use the Supabase CLI to push this as a migration: supabase migration new create_api_keys_table, add the SQL, then supabase db push.</li>
</ul>
<hr />
<h2 id="heading-step-3-create-an-api-key-server-side">Step 3: Create an API Key Server-Side</h2>
<p>This function creates a key. It takes client_id and name, generates everything, and returns the full key once.</p>
<p>Add this in a migration or SQL editor:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> create_api_key(client_id <span class="hljs-keyword">UUID</span>, key_name <span class="hljs-built_in">TEXT</span>)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  ksuid_prefix <span class="hljs-built_in">TEXT</span> := gen_random_ksuid_microsecond();
  secret TEXT := encode(gen_random_bytes(16), 'base32'); <span class="hljs-comment">-- random secret</span>
  salt_val BYTEA := gen_random_bytes(16);
  full_key TEXT := ksuid_prefix || secret;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> api_keys (<span class="hljs-keyword">id</span>, client_id, <span class="hljs-keyword">name</span>, secret_hash, <span class="hljs-keyword">salt</span>, last4)
  <span class="hljs-keyword">VALUES</span> (
    ksuid_prefix,
    client_id,
    key_name,
    digest(secret || salt_val, <span class="hljs-string">'sha256'</span>),
    salt_val,
    <span class="hljs-keyword">right</span>(secret, <span class="hljs-number">4</span>)
  );
  RETURN full_key; <span class="hljs-comment">-- show this to the user once</span>
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>The database makes the secret. Show it once in your app.</p>
<hr />
<h2 id="heading-step-4-verify-api-keys-with-supabase-edge-functions">Step 4: Verify API Keys with Supabase Edge Functions</h2>
<p>To check keys, use a Supabase Edge Function. This is fast and secure. Create one with the Supabase CLI:</p>
<pre><code class="lang-bash">supabase <span class="hljs-built_in">functions</span> new verify-api-key
</code></pre>
<p>Edit functions/verify-api-key/index.ts with Supabase JS:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2'</span>;

<span class="hljs-keyword">const</span> supabase = createClient(Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>)!, Deno.env.get(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>)!);

Deno.serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">const</span> { full_key } = <span class="hljs-keyword">await</span> req.json();
  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.rpc(<span class="hljs-string">'verify_api_key'</span>, { full_key });

  <span class="hljs-keyword">if</span> (error || !data) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">error</span>: <span class="hljs-string">'Invalid key'</span> }), { <span class="hljs-attr">status</span>: <span class="hljs-number">401</span> });
  }

  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-attr">client_id</span>: data }), { <span class="hljs-attr">status</span>: <span class="hljs-number">200</span> });
});
</code></pre>
<p>Deploy it: supabase functions deploy verify-api-key.</p>
<p>The verify_api_key function (add to DB):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> verify_api_key(full_key <span class="hljs-built_in">TEXT</span>)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">UUID</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  ksuid_prefix <span class="hljs-built_in">TEXT</span> := <span class="hljs-keyword">left</span>(full_key, <span class="hljs-number">27</span>);
  secret TEXT := substr(full_key, 28);
  stored_hash BYTEA;
  stored_salt BYTEA;
  key_owner UUID;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">SELECT</span> secret_hash, <span class="hljs-keyword">salt</span>, client_id
  <span class="hljs-keyword">INTO</span> stored_hash, stored_salt, key_owner
  <span class="hljs-keyword">FROM</span> api_keys
  <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = ksuid_prefix;

  IF NOT FOUND THEN
    RETURN NULL;
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

  IF digest(secret || stored_salt, 'sha256') = stored_hash THEN
    RETURN key_owner; <span class="hljs-comment">-- valid: return owner id</span>
  ELSE
    RETURN NULL;       <span class="hljs-comment">-- invalid</span>
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>This is efficient: one index lookup and one hash. See <a target="_blank" href="https://supabase.com/docs/guides/functions">Supabase Functions docs</a> for more.</p>
<hr />
<h2 id="heading-step-5-list-keys-for-your-dashboard">Step 5: List Keys for Your Dashboard</h2>
<p>This function lists keys, sorted by time.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> list_api_keys(
  client_id <span class="hljs-keyword">UUID</span>,
  limit_count <span class="hljs-built_in">INT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">50</span>,
  offset_count <span class="hljs-built_in">INT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TABLE</span>(<span class="hljs-keyword">id</span> <span class="hljs-built_in">TEXT</span>, <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span>, last4 <span class="hljs-built_in">TEXT</span>, created_at TIMESTAMPTZ) <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">RETURN</span> <span class="hljs-keyword">QUERY</span>
  <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>, <span class="hljs-keyword">name</span>, last4, created_at
  <span class="hljs-keyword">FROM</span> api_keys
  <span class="hljs-keyword">WHERE</span> client_id = list_api_keys.client_id
  <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">DESC</span>
  <span class="hljs-keyword">LIMIT</span> limit_count <span class="hljs-keyword">OFFSET</span> offset_count;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>Call it from your app with Supabase JS:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.rpc(<span class="hljs-string">'list_api_keys'</span>, { <span class="hljs-attr">client_id</span>: user.id });
</code></pre>
<hr />
<h2 id="heading-how-your-app-uses-these-in-supabase">How Your App Uses These in Supabase</h2>
<h3 id="heading-create-a-key">Create a Key</h3>
<ul>
<li>App calls: <code>const { data } = await supabase.rpc('create_api_key', { client_id: user.id, key_name: 'My Key' });</code></li>
<li>Show data once to the user.</li>
</ul>
<h3 id="heading-verify-a-key">Verify a Key</h3>
<ul>
<li>Send full_key to your Edge Function.</li>
<li>If it returns client_id, allow access.</li>
</ul>
<h3 id="heading-list-keys">List Keys</h3>
<ul>
<li>Use the list function in your dashboard.</li>
</ul>
<hr />
<h2 id="heading-additional-notes-on-security-and-choices">Additional Notes on Security and Choices</h2>
<ul>
<li>You cannot get back secrets. This is safer.</li>
<li>Add revocation: Include revoked_at in the table and check in verify_api_key.</li>
<li>For rate limits, log usage after verify succeeds.</li>
</ul>
<p>This setup makes secure API key management in Supabase simple and fast.</p>
<p>Ready to try? Start with the Supabase CLI and build your own.</p>
]]></content:encoded></item><item><title><![CDATA[SupaBrain – When Supabase Got Too Fast]]></title><description><![CDATA[In the world of databases, speed is usually a virtue. But what happens when your database is so lightning fast that it leaves developers questioning reality? Enter Supabase, a platform so performant that queries finish their work before you even have...]]></description><link>https://blog.mansueli.com/supabrain-when-supabase-got-too-fast</link><guid isPermaLink="true">https://blog.mansueli.com/supabrain-when-supabase-got-too-fast</guid><category><![CDATA[brainfuck]]></category><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[plv8]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 01 Apr 2025 08:00:23 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742988413136/b063d385-a2e7-4395-ae8c-1830c2f85d46.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the world of databases, speed is usually a virtue. But what happens when your database is so lightning fast that it leaves developers questioning reality? Enter <a target="_blank" href="https://supabase.com/">Supabase</a>, a platform so performant that queries finish their work before you even have time to sip your coffee. To playfully address this <code>problem</code>, we built <strong>SupaBrain</strong>—a Trusted Language Extension (TLE) for PostgreSQL that brings the mind-bending power of BrainFuck right into your database. Who needs traditional SQL when you can execute a language designed to baffle even the most seasoned engineers?</p>
<h2 id="heading-why-brainfuck-why-not">Why BrainFuck? Why Not?</h2>
<p>PostgreSQL has long been celebrated for its extensibility. So why stick with conventional languages like SQL, <code>PL/pgSQL</code>, or even Python when you can support something as esoteric as BrainFuck? With its minimalist syntax (just 8 commands) and unparalleled debugging experience where every error is considered a <code>feature</code>, BrainFuck offers:</p>
<ul>
<li><strong>Minimalist syntax:</strong> Finally, a language where <code>SELECT *</code> is far too verbose.</li>
<li><strong>Unparalleled debugging experience:</strong> Errors in BrainFuck keep you on your toes—every mistake is practically an art form.</li>
<li><strong>Legacy compatibility:</strong> Perfect for those rare moments when you need to integrate with systems dating back to the era of teleprinters.</li>
</ul>
<h2 id="heading-introducing-the-brainfuck-function">Introducing the <code>brainfuck()</code> Function</h2>
<p>The crown jewel of SupaBrain is the <code>brainfuck(code text, input text)</code> function, which lets you execute BrainFuck code directly within PostgreSQL using PLV8. Here’s a simple example:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> brainfuck(<span class="hljs-string">',[.[-],]'</span>, <span class="hljs-string">'Hello!'</span>);
</code></pre>
<p>This snippet will output <code>"Hello!"</code> -- right after PostgreSQL takes a brief existential pause.</p>
<h3 id="heading-the-code-behind-the-magic">The Code Behind the Magic</h3>
<p>Below is the full implementation of the <code>brainfuck()</code> function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> brainfuck(code <span class="hljs-built_in">text</span>, <span class="hljs-keyword">input</span> <span class="hljs-built_in">text</span>)
 <span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">text</span>
 <span class="hljs-keyword">LANGUAGE</span> plv8
 IMMUTABLE <span class="hljs-keyword">STRICT</span>
<span class="hljs-keyword">AS</span> $<span class="hljs-keyword">function</span>$
  <span class="hljs-keyword">var</span> tokens = {
    <span class="hljs-string">"."</span>: <span class="hljs-string">"output += String.fromCharCode(tape[pointer]);"</span>,
    <span class="hljs-string">","</span>: <span class="hljs-string">"tape[pointer] = input.length ? input.shift().charCodeAt(0) : 0;"</span>,
    <span class="hljs-string">"&lt;"</span>: <span class="hljs-string">"pointer = pointer ? pointer - 1 : 255;"</span>,
    <span class="hljs-string">"&gt;"</span>: <span class="hljs-string">"pointer = (pointer + 1) % 256;"</span>,
    <span class="hljs-string">"-"</span>: <span class="hljs-string">"tape[pointer] = tape[pointer] ? tape[pointer] - 1 : 255;"</span>,
    <span class="hljs-string">"+"</span>: <span class="hljs-string">"tape[pointer] = tape[pointer] ? (tape[pointer] + 1) % 256 : 1;"</span>,
    <span class="hljs-string">"["</span>: <span class="hljs-string">"while(tape[pointer]){"</span>,
    <span class="hljs-string">"]"</span>: <span class="hljs-string">"}"</span>
  };
  if (!input) {
    input = "";
  }
  var inner_code = "";
  for (var i=0; i&lt;code.length; i++){
    if (tokens[code[i]]) {
      inner_code += tokens[code[i]] + "\n";
    }
  }
  var js_code = "input = Array.from(input + '');\nvar output = '';\nvar tape = [];\nvar pointer = 0;\n" + 
    inner_code + "return output;";
  var fn = new Function("input", js_code);
  return fn(input.split(','));
$function$;
</code></pre>
<p>This implementation maps each BrainFuck token to a corresponding JavaScript snippet, effectively creating a bridge between BrainFuck and PostgreSQL. By compiling BrainFuck code into JavaScript, SupaBrain enables the execution of esoteric programs in your database.</p>
<h2 id="heading-real-world-applications">Real-World "Applications"</h2>
<p>While SupaBrain is clearly a tongue-in-cheek experiment, its potential applications are as imaginative as they are impractical:</p>
<ul>
<li><p><strong>Query Optimization:</strong><br />Tired of instantaneous results? Implement a BrainFuck-based execution engine to slow things down. Your queries now run with the deliberation of geological time scales.</p>
</li>
<li><p><strong>Data Encryption (Sort of):</strong><br />Secure your data not with advanced cryptography, but with sheer unreadability. If no one can decipher your BrainFuck logic, your secrets are safe by default.</p>
</li>
<li><p><strong>Hiring Filter:</strong><br />Ever been frustrated by a candidate's inability to appreciate complexity? If someone voluntarily writes queries in BrainFuck, congratulations—you’ve likely found your next 10x engineer.</p>
</li>
</ul>
<h2 id="heading-performance-considerations-or-lack-thereof">Performance Considerations (or Lack Thereof)</h2>
<p>Traditional SQL queries finish in milliseconds. With BrainFuck, you’re invited to experience time in a whole new dimension.</p>
<p><strong>Why do:</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment">-- 0.1ms </span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-number">1</span>;
</code></pre>
<p><strong>When you can:</strong></p>
<pre><code class="lang-sql"><span class="hljs-comment">-- 1.7 billion years</span>
<span class="hljs-keyword">SELECT</span> brainfuck(<span class="hljs-string">'[-]+&gt;[-]+&gt;[-]+&gt;[-]+&gt;[-]+&gt;[-]+&lt;&lt;[&gt;&gt;&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;[-]+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&gt;[-]+&gt;[-]+&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]]&gt;&gt;[[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]]&gt;&gt;[[-]]&gt;&gt;&gt;&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;[-]++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++&gt;[-]+&gt;[-]+&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]]&gt;&gt;[[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]]&gt;&gt;[[-]]&gt;&gt;&gt;&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;[-]++++++++++&gt;[-]+&gt;[-]+&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&gt;&gt;&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;-]&gt;[-]+&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;[&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;-]&lt;[&lt;[-]&gt;[-]]&lt;&lt;&lt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]]&gt;&gt;[[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]]&gt;&gt;[[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]]&gt;&gt;[[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]+&gt;[-]&gt;&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&gt;+&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;[-]&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&gt;&gt;+&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;[&gt;[&lt;&lt;[-]+&gt;&gt;[-]]&lt;[-]]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]&lt;&lt;&lt;&lt;&lt;[&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;+&lt;&lt;&lt;&lt;&lt;-]&gt;&gt;&gt;&gt;&gt;[&lt;&lt;&lt;&lt;&lt;+&gt;&gt;&gt;&gt;&gt;-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;]
, '');</span>
</code></pre>
<p>The performance trade-off is clear: if you desire to truly savor the passage of time, SupaBrain’s execution will ensure that every moment counts. And don’t worry about scalability—if anything, it scales logarithmically… downwards.</p>
<h2 id="heading-installation-making-postgresql-regret-its-flexibility">Installation: Making PostgreSQL Regret Its Flexibility</h2>
<p>Before proceeding, ensure you have the following installed:</p>
<ul>
<li><strong>PLV8:</strong> Required for running JavaScript code within PostgreSQL.</li>
<li><strong>Database.dev:</strong> Provides access to the PostgreSQL package manager and trusted language extensions (TLE).</li>
</ul>
<h3 id="heading-installing-the-trusted-language-extension-tle">Installing the Trusted Language Extension (TLE)</h3>
<p>If you don't have the TLE installed already, follow these steps:</p>
<pre><code class="lang-sql">
<span class="hljs-comment">-- Install TLE if not already installed</span>
<span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> <span class="hljs-keyword">http</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">schema</span> extensions;
<span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> pg_tle;
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span> pgtle.uninstall_extension_if_exists(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">select</span>
    pgtle.install_extension(
        <span class="hljs-string">'supabase-dbdev'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'version'</span>,
        <span class="hljs-string">'PostgreSQL package manager'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'sql'</span>
    )
<span class="hljs-keyword">from</span> <span class="hljs-keyword">http</span>(
    (
        <span class="hljs-string">'GET'</span>,
        <span class="hljs-string">'https://api.database.dev/rest/v1/'</span>
        || <span class="hljs-string">'package_versions?select=sql,version'</span>
        || <span class="hljs-string">'&amp;package_name=eq.supabase-dbdev'</span>
        || <span class="hljs-string">'&amp;order=version.desc'</span>
        || <span class="hljs-string">'&amp;limit=1'</span>,
        <span class="hljs-built_in">array</span>[
            (<span class="hljs-string">'apiKey'</span>, <span class="hljs-string">'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhtdXB0cHBsZnZpaWZyYndtbXR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODAxMDczNzIsImV4cCI6MTk5NTY4MzM3Mn0.z2CN0mvO2No8wSi46Gw59DFGCTJrzM0AQKsu_5k134s'</span>)::http_header
        ],
        <span class="hljs-literal">null</span>,
        <span class="hljs-literal">null</span>
    )
) x,
<span class="hljs-keyword">lateral</span> (
    <span class="hljs-keyword">select</span>
        ((row_to_json(x) -&gt; <span class="hljs-string">'content'</span>) <span class="hljs-comment">#&gt;&gt; '{}')::json -&gt; 0</span>
) resp(<span class="hljs-keyword">contents</span>);
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span> dbdev.install(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
</code></pre>
<h3 id="heading-installing-the-brainfuck-tle">Installing the BrainFuck TLE</h3>
<p>Once the TLE is in place, install the BrainFuck extension:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Install the BrainFuck extension using the database package manager</span>
<span class="hljs-keyword">SELECT</span> dbdev.install(<span class="hljs-string">'mansueli@brainfuck'</span>);
<span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-string">"mansueli@brainfuck"</span>
    <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> <span class="hljs-comment">-- You can change it here to something else</span>
    <span class="hljs-keyword">VERSION</span> <span class="hljs-string">'4.2.0'</span>;
</code></pre>
<p><em>⚠️ Warning:</em> Installing this extension <strong>may void your database's warranty.</strong></p>
<h3 id="heading-example-function-using-brainfuck">Example Function Using BrainFuck</h3>
<p>Here’s an example that uses the BrainFuck TLE to format a date:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> public.format_date(input_date <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">text</span>
<span class="hljs-keyword">LANGUAGE</span> plpgsql
IMMUTABLE <span class="hljs-keyword">STRICT</span>
<span class="hljs-keyword">AS</span> $<span class="hljs-keyword">function</span>$
<span class="hljs-keyword">BEGIN</span>
<span class="hljs-keyword">return</span> brainfuck(<span class="hljs-string">',&gt;,&gt;,&gt;,&gt;,&gt;,&gt;,&lt;&lt;,&gt;&gt;&gt;,&gt;,&gt;&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&gt;[-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+++++++&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+++++++++&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+++++++++&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+++++++&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;[-]&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-]++++++++++++[-&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;++++++++++&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;+&gt;[-]&gt;[-]&gt;[-][-]&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-][-]&gt;&gt;[-]++++[-&lt;&lt;++++++++++&gt;&gt;]&lt;&lt;+++++++.&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[-][-]&gt;&gt;[-]++++[-&lt;&lt;++++++++++&gt;&gt;]&lt;&lt;+++++++.&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;&lt;.&gt;.&gt;.&gt;.&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;&gt;[.&gt;]'</span>,input_date::<span class="hljs-built_in">text</span>);
<span class="hljs-keyword">END</span>;
$function$
</code></pre>
<h2 id="heading-the-future-of-supabrain">The Future of SupaBrain</h2>
<p>The journey of SupaBrain has just begun. Some exciting ideas for future enhancements include:</p>
<ul>
<li><p><strong>AI-Powered Code Generation:</strong><br />Imagine a world where ChatGPT refuses to generate BrainFuck code—simply citing ethical concerns. The irony isn’t lost on us.</p>
</li>
<li><p><strong>Multi-threaded BrainFuck Execution:</strong><br />While PostgreSQL isn’t ready for it yet, we dream of a day when BrainFuck can run concurrently across multiple threads.</p>
</li>
<li><p><strong>BrainFuck ORM:</strong><br />Stay tuned for our BrainFuck Object-Relational Mapping tool. It’s coming soon—just as soon as we figure out how to map tables to tape memory.</p>
</li>
</ul>
<h2 id="heading-conclusion-the-feature-you-never-knew-you-didnt-need">Conclusion: The Feature You Never Knew You Didn’t Need</h2>
<p>SupaBrain transforms your database experience into something frustratingly unique. Whether it’s for a quirky hiring filter, or simply to prove that <strong>just because you can, doesn’t mean you should</strong>, SupaBrain has a place in every developer’s toolkit. So, why not give it a try? Or don’t—we promise not to judge.</p>
<p>Happy coding (or decoding)!</p>
]]></content:encoded></item><item><title><![CDATA[Executing Dynamic JavaScript Code on Supabase with Edge Functions]]></title><description><![CDATA[Supabase is a powerful backend service that makes it easy for developers to work with real-time data, authentication, and more. One of the most interesting features it offers is Supabase Edge Functions, which allow you to create and run serverless fu...]]></description><link>https://blog.mansueli.com/executing-dynamic-javascript-code-on-supabase-with-edge-functions</link><guid isPermaLink="true">https://blog.mansueli.com/executing-dynamic-javascript-code-on-supabase-with-edge-functions</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Edge-Functions]]></category><category><![CDATA[edgecomputing]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[embedding]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Thu, 21 Nov 2024 12:16:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1732191316502/18c82853-6d72-4a41-a7ac-1d55ec369584.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="http://supabase.com/">Supabase</a> is a powerful backend service that makes it easy for developers to work with real-time data, authentication, and more. One of the most interesting features it offers is <a target="_blank" href="https://supabase.com/docs/guides/functions">Supabase Edge Functions</a>, which allow you to create and run serverless functions close to the users, reducing latency and improving the overall experience.</p>
<p>We're always looking for ways to improve the developer experience and reduce complexity across your application development pipeline. One way you can use Supabase to do that is with dynamic JavaScript in Edge Functions. This greatly increases the versatility of your edge functions and reduces the need for you to redeploy your functions if you need to change business logic.</p>
<h3 id="heading-introduction-to-edge-functions">Introduction to Edge Functions</h3>
<p>Edge Functions in Supabase are serverless functions that execute in response to HTTP requests. These functions are deployed at the edge, meaning they run close to the user's location, resulting in faster response times.</p>
<h3 id="heading-why-use-dynamic-code-execution">Why Use Dynamic Code Execution?</h3>
<p>Dynamic code execution allows you to modify and run JavaScript code on the fly without having to redeploy your function each time the code changes. This is particularly useful when you need the flexibility to execute different logic depending on the incoming request, without incurring the overhead of redeployment.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>To follow along, you will need:</p>
<ul>
<li>A Supabase project</li>
<li>Supabase CLI installed on your local machine</li>
<li>Orb Stack or Docker Desktop installed on your local machine</li>
<li>Environment variables set up in Vault, ensuring it passes validation in the function (e.g., <code>service_role</code>)</li>
</ul>
<blockquote>
<p>Edge Functions defaults to the verification of the JWT, so it could be called with the ANON API Key. Make sure to implement proper security measures.</p>
</blockquote>
<h2 id="heading-install-the-sql-script-from-the-repo">Install the SQL script from the repo</h2>
<p>We have a repo with the SQL script to create helper functions to support the dynamic execution of JavaScript code. You can find the repo here: <a target="_blank" href="https://github.com/mansueli/supa-dynamic">supa-dynamic</a></p>
<p>Install the SQL script <code>supa-dynamic--0.1.sql</code> from the repo in your Supabase project. (You can copy and paste the code from the repo into the SQL editor in your Supabase project.)
These are the functions we'll use to execute the JavaScript code:</p>
<ul>
<li><code>edge.http_request(url text, method text, headers jsonb, params jsonb, payload jsonb, timeout_ms integer) RETURNS jsonb</code>: Makes an HTTP request with the specified parameters.</li>
<li><code>edge_wrapper(code text) RETURNS text</code>: Executes the provided JavaScript code.</li>
<li><code>edge.get_secret(secret_name text) RETURNS text</code>: Retrieves a secret from Vault.</li>
</ul>
<h3 id="heading-deep-dive-into-the-helper-functions-optional">Deep Dive into the helper functions (optional)</h3>
<p>You can skip this section if you are only interested in using the dynamic execution of JavaScript code. However, if you want to understand how the helper functions work, keep reading.</p>
<h3 id="heading-edgehttprequest-function"><code>edge.http_request</code> Function</h3>
<p>This function handles the actual HTTP request and processes the response. It ensures consistency in response format.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> edge.http_request(
    <span class="hljs-keyword">url</span> <span class="hljs-built_in">TEXT</span>,
    method <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'POST'</span>,
    headers JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{"Content-Type": "application/json"}'</span>::jsonb,
    params JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
    payload JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
    timeout_ms <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">5000</span>
) <span class="hljs-keyword">RETURNS</span> jsonb <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    http_response extensions.http_response;
    status_code integer := 0;
    response_json jsonb;
    response_text text;
    header_array extensions.http_header[];
    request extensions.http_request;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Set the timeout option</span>
    <span class="hljs-keyword">IF</span> timeout_ms &gt; <span class="hljs-number">0</span> <span class="hljs-keyword">THEN</span>
        PERFORM http_set_curlopt(<span class="hljs-string">'CURLOPT_TIMEOUT_MS'</span>, timeout_ms::<span class="hljs-built_in">text</span>);
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Convert headers JSONB to http_header array</span>
    <span class="hljs-keyword">SELECT</span> array_agg(extensions.http_header(<span class="hljs-keyword">key</span>, <span class="hljs-keyword">value</span>::<span class="hljs-built_in">text</span>))
    <span class="hljs-keyword">FROM</span> jsonb_each_text(headers)
    <span class="hljs-keyword">INTO</span> header_array;

    <span class="hljs-comment">-- Construct the http_request composite type</span>
    request := ROW(method, url, header_array, 'application/json', payload::text)::extensions.http_request;

    <span class="hljs-comment">-- Make the HTTP request</span>
    http_response := http(request);
    status_code := http_response.status;

    <span class="hljs-comment">-- Attempt to extract JSONB response content</span>
    <span class="hljs-keyword">BEGIN</span>
        response_json := http_response.content::jsonb;
    EXCEPTION
        WHEN others THEN
            response_text := http_response.content;
            response_json := jsonb_build_object('status_code', status_code, 'response', response_text);
    <span class="hljs-keyword">END</span>;

    RETURN jsonb_build_object('status_code', status_code, 'response', response_json);
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<h3 id="heading-edgewrapper-function"><code>edge_wrapper</code> Function</h3>
<p>The <code>edge_wrapper</code> function manages HTTP requests with features like retries, custom headers, and region selection. Below are the parameters it accepts:</p>
<ul>
<li><strong><code>url</code></strong>: The endpoint to call.</li>
<li><strong><code>method</code></strong>: HTTP method, defaulting to <code>POST</code>.</li>
<li><strong><code>headers</code></strong>: Custom headers to include, including region information.</li>
<li><strong><code>timeout_ms</code></strong>: Timeout duration in milliseconds.</li>
<li><strong><code>max_retries</code></strong>: Maximum retry attempts for the request.</li>
</ul>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> edge.edge_wrapper(
    <span class="hljs-keyword">url</span> <span class="hljs-built_in">TEXT</span>,
    method <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'POST'</span>,
    headers JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{"Content-Type": "application/json"}'</span>::jsonb,
    params JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
    payload JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb, <span class="hljs-comment">-- Payload as JSONB</span>
    timeout_ms <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">5000</span>,
    max_retries <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,
    allowed_regions <span class="hljs-built_in">TEXT</span>[] <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">NULL</span>
) <span class="hljs-keyword">RETURNS</span> jsonb <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    retry_count <span class="hljs-built_in">INTEGER</span> := <span class="hljs-number">0</span>;
    retry_delays DOUBLE PRECISION[] := ARRAY[0, 0.250, 0.500, 1.000, 2.500, 5.000];
    succeeded BOOLEAN := FALSE;
    current_region_index INTEGER := 1; <span class="hljs-comment">-- Start index at 1 for PostgreSQL array</span>
    combined_headers JSONB;
    response_json JSONB;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Validate headers, params, and payload are JSON objects</span>
    <span class="hljs-keyword">IF</span> headers <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">NOT</span> jsonb_typeof(headers) = <span class="hljs-string">'object'</span> <span class="hljs-keyword">THEN</span>
        <span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">EXCEPTION</span> <span class="hljs-string">'Invalid headers parameter: %'</span>, headers;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    IF params IS NULL OR NOT jsonb_typeof(params) = 'object' THEN
        RAISE EXCEPTION 'Invalid params parameter: %', params;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    IF payload IS NULL OR NOT jsonb_typeof(payload) = 'object' THEN
        RAISE EXCEPTION 'Invalid payload parameter: %', payload;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Validate allowed_regions if provided</span>
    IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) = 0 THEN
        RAISE EXCEPTION 'allowed_regions parameter cannot be an empty array';
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Check if retry_delays has enough elements</span>
    IF cardinality(retry_delays) &lt; max_retries + 1 THEN
        RAISE EXCEPTION 'retry_delays array must have at least % elements', max_retries + 1;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Retry loop</span>
    WHILE NOT succeeded AND retry_count &lt;= max_retries LOOP
        combined_headers := headers;

        <span class="hljs-comment">-- Set x-region header if allowed_regions is provided</span>
        IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) &gt; 0 THEN
            combined_headers := combined_headers || jsonb_build_object('x-region', allowed_regions[current_region_index]);
        <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

        <span class="hljs-comment">-- Sleep if not the first attempt</span>
        IF retry_count &gt; 0 THEN
            PERFORM pg_sleep(retry_delays[retry_count]);
        <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

        retry_count := retry_count + 1;

        <span class="hljs-comment">-- Increment region index, wrapping around if necessary</span>
        IF allowed_regions IS NOT NULL AND cardinality(allowed_regions) &gt; 0 THEN
            current_region_index := current_region_index + 1;
            IF current_region_index &gt; cardinality(allowed_regions) THEN
                current_region_index := 1;
            <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
        <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

        <span class="hljs-keyword">BEGIN</span>
            <span class="hljs-keyword">RAISE</span> <span class="hljs-keyword">WARNING</span> <span class="hljs-string">'headers:%s'</span>, combined_headers;

            <span class="hljs-comment">-- Call the simplified HTTP request function</span>
            response_json := edge.http_request(url, method, combined_headers, params, payload, timeout_ms);

            <span class="hljs-comment">-- Check the status code</span>
            IF (response_json-&gt;&gt;'status_code')::INTEGER &lt; 500 THEN
                succeeded := TRUE;
            <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
        EXCEPTION
            WHEN OTHERS THEN
                IF retry_count &gt; max_retries THEN
                    RAISE EXCEPTION 'HTTP request failed after % retries. SQL Error: { %, % }',
                        max_retries, SQLERRM, SQLSTATE;
                <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
        <span class="hljs-keyword">END</span>;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;

    RETURN response_json;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>To securely manage secrets, you will need to set your <code>service_role_key</code> in Vault. Here’s how you can create a function to retrieve secrets:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> edge.get_secret(secret_name <span class="hljs-built_in">text</span>) <span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">text</span>
    <span class="hljs-keyword">LANGUAGE</span> <span class="hljs-string">"plpgsql"</span>
    <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    decrypted <span class="hljs-built_in">text</span>;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> current_setting(<span class="hljs-string">'request.jwt.claims'</span>, <span class="hljs-literal">true</span>)::jsonb-&gt;&gt;<span class="hljs-string">'role'</span> = <span class="hljs-string">'service_role'</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">current_user</span> = <span class="hljs-string">'postgres'</span> <span class="hljs-keyword">THEN</span>
        <span class="hljs-keyword">SELECT</span> decrypted_secret
        <span class="hljs-keyword">INTO</span> decrypted
        <span class="hljs-keyword">FROM</span> vault.decrypted_secrets
        <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = secret_name;
        RETURN decrypted;
    ELSE
        RAISE EXCEPTION 'Access denied: only service_role or postgres user can <span class="hljs-keyword">execute</span> this function.<span class="hljs-string">';
    END IF;
END;
$$;</span>
</code></pre>
<p>This function can retrieve the <code>service_role</code> secret from <a target="_blank" href="https://supabase.com/dashboard/project/_/settings/vault/secrets">Vault</a>, it also ensures that only authorized roles can access sensitive environment variables.</p>
<h2 id="heading-setting-up-the-edge-function">Setting Up the Edge Function</h2>
<p>Let's dive into the code and set up our dynamic JavaScript executor Edge Function using Deno. Below is an overview of how to accomplish this.</p>
<h3 id="heading-code-walkthrough">Code Walkthrough</h3>
<p>We'll create a function named <code>multi-purpose</code>:</p>
<pre><code class="lang-jsx">supabase functions <span class="hljs-keyword">new</span> multi-purpose
</code></pre>
<p>Now, we'll edit the code adding verification and the eval function, including the supabase client so we have it ready without the need to import.</p>
<pre><code class="lang-tsx">import "jsr:@supabase/functions-js/edge-runtime.d.ts";

// Import the supabase client
import { createClient } from "&lt;https://esm.sh/@supabase/supabase-js@2&gt;";

console.log("===\\n\\tBooted Edge Worker!\\n===\\n");
const supabase_url = Deno.env.get("SUPABASE_URL") ?? "";
const service_role = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
// Set the permission to service_role key:
const supabase = createClient(supabase_url, service_role);
// This allows us to use Supabase.ai in the function
const session = new Supabase.ai.Session('gte-small');

Deno.serve(async (req: Request) =&gt;
  const authorization = req.headers.get("Authorization");
  if (!authorization) throw new Error("Authorization header is missing.");
  // Ensures that the function is called with service_role to prevent missuse
  if (!authorization.includes(service_role)) {
    throw new Error("Authorization header is invalid.");
  }

  const { code } = await req.json();
  try {
    // Wrap the provided code in an async function context
    const asyncFunction = new Function('supabase', `
      return (async () =&gt; {
        ${code.replace(/\\\\/g, '')}
      })();
    `);
    // Pass the Supabase client as the scope for the function to use:
    const data = await asyncFunction(supabase);
    console.log(data);
    return new Response(
      JSON.stringify({ data }),
      { headers: { 'Content-Type': 'application/json', 'Connection': 'keep-alive' } },
    );
  } catch (error) {
    console.error("Error executing user code:", error);
    return new Response(
      JSON.stringify({ error: "An error occurred -&gt; " + error.message }),
      { status: 500, headers: { "Content-Type": "application/json" } }
    );
  }
});
</code></pre>
<blockquote>
<p>Note: If you need more details, check the full guide to <a target="_blank" href="https://supabase.com/docs/guides/functions/quickstart#create-an-edge-function">create an edge function</a>.</p>
</blockquote>
<h3 id="heading-step-by-step-walkthrough">Step-by-Step Walkthrough</h3>
<ol>
<li><strong>Validate Authorization</strong>: First, we ensure the request contains a valid authorization header. (this prevents calls from anon users)</li>
</ol>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> authorization = req.headers.get(<span class="hljs-string">'Authorization'</span>)
<span class="hljs-keyword">if</span> (!authorization) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Authorization header is missing.'</span>)
<span class="hljs-comment">// Ensures that the function is called with service_role to prevent missuse</span>
<span class="hljs-keyword">if</span> (!authorization.includes(service_role)) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Authorization header is invalid.'</span>)
}
</code></pre>
<ol>
<li><strong>Receive JavaScript Code Payload</strong>: Extract the <code>code</code> from the request body.</li>
</ol>
<pre><code class="lang-jsx"><span class="hljs-keyword">const</span> { code } = <span class="hljs-keyword">await</span> req.json()
</code></pre>
<ol>
<li><strong>Wrap Code in Async Context</strong>: Use <code>new Function()</code> to create an async function that executes the incoming JavaScript code. This allows async calls in the code to be executed:</li>
</ol>
<pre><code class="lang-jsx"><span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Wrap the provided code in an async function context</span>
    <span class="hljs-keyword">const</span> asyncFunction = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Function</span>(<span class="hljs-string">'supabase'</span>, <span class="hljs-string">`
      return (async () =&gt; {
        <span class="hljs-subst">${code.replace(<span class="hljs-regexp">/\\\\/g</span>, <span class="hljs-string">''</span>)}</span>
      })();
    `</span>);
}
</code></pre>
<ol>
<li><strong>Execute and Return Results</strong>: Run the JavaScript code, which can interact with Supabase via the provided client, and return the results.</li>
</ol>
<pre><code class="lang-jsx"><span class="hljs-comment">// Pass the Supabase client as the scope for the function to use:</span>
<span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> asyncFunction(supabase)
<span class="hljs-built_in">console</span>.log(data)
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ data }), {
  <span class="hljs-attr">headers</span>: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>, <span class="hljs-attr">Connection</span>: <span class="hljs-string">'keep-alive'</span> },
})
</code></pre>
<h3 id="heading-deploying-the-edge-function">Deploying the Edge Function</h3>
<p>To deploy this Edge Function, you'll need to use the Supabase CLI. Ensure you have Docker installed and running on your local machine. Follow these steps to deploy:</p>
<ol>
<li><strong>Install the Supabase CLI</strong>: If you haven't already, install the Supabase CLI by following the instructions in the <a target="_blank" href="https://supabase.com/docs/guides/cli">Supabase CLI Documentation</a>.</li>
<li><strong>Log In to Supabase</strong>: Use the command <code>supabase login</code> to authenticate your account.</li>
<li><strong>Deploy the Function</strong>: Run the command <code>supabase functions deploy &lt;function_name&gt;</code> to deploy your Edge Function. Replace <code>&lt;function_name&gt;</code> with the desired name for your function.</li>
</ol>
<h3 id="heading-setting-environment-variables-in-vault">Setting Environment Variables in Vault</h3>
<h2 id="heading-creating-the-main-function-to-interact-with-the-edge-function">Creating the main function to interact with the edge function</h2>
<p>We are using the helper functions defined earlier to create a function that interacts with the edge function. This function will execute the dynamic JavaScript code and return the results.
This is the main function that will be used to execute the dynamic JavaScript code and return the results.</p>
<h3 id="heading-edgeexec-function"><code>edge.exec</code> Function</h3>
<p>The <code>edge.exec</code> is a simple function leverages <code>edge_wrapper</code> to execute dynamic JavaScript code. Here's an example of how it is structured:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> edge.exec(<span class="hljs-keyword">data</span> <span class="hljs-built_in">text</span>) <span class="hljs-keyword">RETURNS</span> JSONB <span class="hljs-keyword">LANGUAGE</span> plpgsql
<span class="hljs-keyword">AS</span> $<span class="hljs-keyword">function</span>$
<span class="hljs-keyword">DECLARE</span>
    custom_headers JSONB;
<span class="hljs-comment">-- Example restricting regions available to Europe</span>
    allowed_regions TEXT[] := ARRAY['eu-west-1', 'eu-west-2', 'eu-west-3', 'eu-north-1', 'eu-central-1'];
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Set headers with anon key and Content-Type</span>
    custom_headers := jsonb_build_object(
        <span class="hljs-string">'Authorization'</span>, <span class="hljs-string">'Bearer '</span> || edge.get_secret(<span class="hljs-string">'service_role_key'</span>),
        <span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/json'</span>,
        <span class="hljs-string">'x-region'</span>, allowed_regions
    );
    <span class="hljs-comment">-- Call edge_wrapper function with default values</span>
    RETURN edge.edge_wrapper(
        url := ('https://&lt;ref&gt;.supabase.co/functions/v1/multi-purpose'),
        headers := custom_headers,
        payload := jsonb_build_object('code', data),
        max_retries := 5,
        allowed_regions := allowed_regions
    );
<span class="hljs-keyword">END</span>;
$function$;
</code></pre>
<h3 id="heading-executing-dynamic-javascript-code">Executing Dynamic JavaScript Code</h3>
<p>The key to executing the dynamic JavaScript code is wrapping it in an <code>async</code> function context using <code>new Function()</code>. This approach lets you evaluate the code in isolation while retaining access to the <code>supabase</code> client for interacting with your database. You can check the examples of how to use this calling the <a target="_blank" href="https://www.notion.so/Executing-Dynamic-JavaScript-Code-on-Supabase-with-Edge-Functions-1125004b775f80ca97d4c367a2cfcd7d?pvs=21">supabase client</a> or even <a target="_blank" href="https://www.notion.so/Executing-Dynamic-JavaScript-Code-on-Supabase-with-Edge-Functions-1125004b775f80ca97d4c367a2cfcd7d?pvs=21">generating embeddings</a>.</p>
<h3 id="heading-example-of-using-supabase-client-libraries">Example of Using Supabase Client Libraries</h3>
<p>To demonstrate the execution of dynamic JavaScript, you can use the Supabase client libraries within the SQL context. Here’s an example query:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> edge.exec(
  $js$
  const { <span class="hljs-keyword">data</span>, <span class="hljs-keyword">error</span> } = await supabase.rpc(<span class="hljs-string">'postgres_function'</span>, {<span class="hljs-string">'foo'</span>: <span class="hljs-string">'bar'</span>});
  if (error) {
    return new Response(JSON.stringify({ error: "An error occurred -&gt;" + error.message }), {
      status: 500,
      headers: { "Content-Type": "application/json" },
    });
  }
  return data;
  $js$
);
</code></pre>
<h2 id="heading-using-the-edge-function-in-practice">Using the Edge Function in Practice</h2>
<h3 id="heading-example-generating-embeddings">Example: Generating Embeddings</h3>
<p>The <code>edge.exec</code> function allows for dynamic JavaScript execution, such as interacting with an AI session to generate embeddings. When executed, the JavaScript code within the SQL context runs through the edge function, returning results to the database.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">select</span> edge.exec(
$js$

const <span class="hljs-keyword">session</span> = <span class="hljs-keyword">new</span> Supabase.ai.Session(<span class="hljs-string">'gte-small'</span>);
return await session.run('hello world');

$js$);
</code></pre>
<p>You can also create a Postgres function to generate embeddings:</p>
<pre><code>CREATE OR REPLACE FUNCTION edge.generate_embedding(input_text TEXT) RETURNS JSONB AS $$
DECLARE
    response JSONB;
BEGIN
    -- Call the edge <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">to</span> <span class="hljs-title">generate</span> <span class="hljs-title">the</span> <span class="hljs-title">embedding</span> <span class="hljs-title">for</span> <span class="hljs-title">the</span> <span class="hljs-title">provided</span> <span class="hljs-title">text</span>
    <span class="hljs-title">response</span> := <span class="hljs-title">edge</span>.<span class="hljs-title">exec</span>(<span class="hljs-params">
        format(
            $js$
            const session = new Supabase.ai.Session(<span class="hljs-string">'gte-small'</span>);
            return await session.run(%L);
            $js$,
            input_text
        )
    </span>);
    <span class="hljs-title">RETURN</span> <span class="hljs-title">response</span>-&gt;'<span class="hljs-title">response</span>'-&gt;'<span class="hljs-title">data</span>';
<span class="hljs-title">END</span>;
<span class="hljs-title">$$</span> <span class="hljs-title">LANGUAGE</span> <span class="hljs-title">plpgsql</span>;

 <span class="hljs-title">select</span> <span class="hljs-title">edge</span>.<span class="hljs-title">generate_embedding</span>(<span class="hljs-params"><span class="hljs-string">'The quick brown fox jumps over the lazy dog'</span></span>);

-- <span class="hljs-title">response</span>:
-- [-0.07254139333963394,-0.02173878252506256,0.042930446565151215,0.04853367060422897,0.015609614551067352,0.02912059798836708,0.0371023565530777,0.05054798722267151,0.0035842431243509054,0.0015563230263069272,0.0009484672918915749,-0.09247169643640518,0.04190639406442642,0.05874202027916908,-0.012341015040874481,0.01661474071443081,-0.013452880084514618,0.003742767730727792,-0.07664268463850021,0.03231268376111984,0.0006968052475713193,-0.06508929282426834,-0.04956015944480896,-0.014327225275337696,0.03270547464489937,0.01635774038732052,-0.022707758471369743,-0.007586371619254351,-0.03548099845647812,-0.17844657599925995,0.03325255215167999,-0.07009242475032806,0.02982083335518837,-0.05649203434586525,-0.006693259347230196,-0.02781110256910324,-0.01687553897500038,0.04976152256131172,-0.015715090557932854,0.038247860968112946,0.040495794266462326,-0.007263457402586937,-0.019288228824734688,-0.0527581050992012,-0.0065462407656013966,-0.022786622866988182,-0.04975651577115059,-0.04053974151611328,0.03047902137041092,-0.05064946785569191,-0.023929744958877563,-0.03891737014055252,0.03785012289881706,-0.0133274607360363,0.03001898154616356,-0.007281183265149593,0.060004156082868576,0.017414024099707603,0.025516854599118233,0.029599720612168312,0.02893918938934803,0.03455337509512901,-0.14698833227157593,0.09387505799531937,0.05768263339996338,0.019130567088723183,-0.0380706787109375,-0.04105521738529205,0.008963614702224731,0.012743324972689152,0.009223062545061111,0.060711149126291275,0.007398003712296486,0.04229794815182686,0.046996768563985825,-0.003397924592718482,0.00808036606758833,0.022617157548666,-0.01847437582910061,0.0026343590579926968,-0.010598739609122276,-0.037673674523830414,-0.04375630244612694,-0.0007789010996930301,-0.007935777306556702,-0.03272915259003639,0.021433845162391663,-0.07967976480722427,0.06888656318187714,0.07489841431379318,-0.02783842757344246,-0.006374717690050602,-0.035476282238960266,0.006344574969261885,-0.03357071802020073,-0.036727335304021835,0.012309364043176174,-0.00006389369809767231,-0.053050097078084946,0.19709722697734833,-0.05575009435415268,0.05757850036025047,0.0951322615146637,-0.04633559286594391,0.03476420044898987,0.012983368709683418,0.0004390157700981945,0.010212302207946777,-0.012741461396217346,0.014706282876431942,0.03321540355682373,-0.006495281588286161,0.041682176291942596,0.003406582633033395,0.02581774815917015,-0.0007246752502396703,0.011133069172501564,0.08353550732135773,0.006477882619947195,0.00224463758058846,0.020395604893565178,-0.013416256755590439,0.05663946643471718,-0.028388522565364838,0.019082417711615562,-0.08387858420610428,0.054498571902513504,0.10694538056850433,0.06286843866109848,0.03180928900837898,0.037740662693977356,-0.07479764521121979,0.010231229476630688,-0.04866624251008034,0.004061027429997921,0.0362103171646595,-0.009540606290102005,0.00915283989161253,0.031154874712228775,-0.04876647889614105,-0.015956921502947807,-0.1429857611656189,-0.01470054779201746,-0.09399641305208206,-0.019157350063323975,0.02896934375166893,-0.018669532611966133,0.014991801232099533,-0.06764508783817291,0.027312103658914566,-0.003859955817461014,0.025718173012137413,-0.018675100058317184,-0.016409857198596,-0.021459592506289482,0.004702075384557247,-0.0323822982609272,0.10394860059022903,-0.020106177777051926,-0.008876764215528965,-0.027185838669538498,0.0003392586368136108,-0.009877108968794346,-0.0004303457390051335,0.04185814782977104,-0.05188998952507973,-0.021185973659157753,0.00026368125691078603,-0.02180171199142933,-0.03400561958551407,0.020068379119038582,0.034275852143764496,-0.10943055897951126,0.031987469643354416,0.054017845541238785,-0.009243185631930828,-0.07103140652179718,0.00785127654671669,-0.0040434580296278,-0.05036382004618645,0.07858535647392273,-0.08356015384197235,-0.06914680451154709,0.06180981919169426,0.043073058128356934,-0.020246226340532303,-0.015496478416025639,-0.005946696270257235,0.006562687456607819,0.04845070466399193,-0.029123008251190186,0.02194702997803688,0.002446065191179514,-0.06825454533100128,-0.07056894898414612,0.01598423719406128,-0.04185032472014427,-0.01633128523826599,0.014294272288680077,-0.01768324337899685,0.05590462312102318,-0.044063832610845566,0.02461099997162819,0.0006756667862646282,0.07429251074790955,0.011551265604794025,0.014212443493306637,-0.02237367257475853,0.039057254791259766,0.000325449975207448,-0.004185846075415611,-0.003040974261239171,0.01800958439707756,-0.02479490265250206,-0.019247515127062798,0.04366869106888771,-0.027130864560604095,0.018955133855342865,0.03239727392792702,0.03226468712091446,0.06487660109996796,-0.06456360220909119,0.0006639647181145847,-0.20788206160068512,0.05066373199224472,-0.012870946899056435,-0.034873317927122116,0.023824242874979973,-0.02305314689874649,0.030056791380047798,-0.06937119364738464,0.0642433762550354,0.05418730527162552,0.06050065532326698,-0.04655877873301506,-0.026898164302110672,-0.003803820814937353,0.002598312683403492,0.1081414744257927,0.014850604347884655,0.013619652017951012,0.013523285277187824,-0.0016119466163218021,-0.00329813570715487,0.002907108049839735,0.014589778147637844,-0.048919934779405594,0.056754376739263535,-0.03171522915363312,0.2308642566204071,0.08356188982725143,0.05350973457098007,-0.03191335126757622,0.003732810029760003,0.031172126531600952,-0.08899383991956711,-0.09938952326774597,0.08256369829177856,0.08178982138633728,0.07785400003194809,-0.04618730768561363,-0.02995850332081318,-0.022348755970597267,-0.05898110195994377,0.05294518917798996,0.0038859194610267878,-0.0923057422041893,-0.01576364040374756,-0.0035308743827044964,-0.04901731014251709,-0.012596397660672665,-0.036502618342638016,0.00886201299726963,0.059619251638650894,-0.017561428248882294,0.05459151417016983,0.04560315981507301,-0.0019153780303895473,0.009595169685781002,-0.057729125022888184,0.026341130957007408,-0.023892194032669067,0.016832968220114708,-0.026450062170624733,-0.07305766642093658,0.03468620404601097,-0.02054707705974579,0.041034333407878876,0.00404499564319849,-0.017474710941314697,-0.043891143053770065,0.02514275535941124,0.02372695878148079,0.010677577927708626,0.06225359067320824,0.040919024497270584,0.005154050886631012,0.030111495405435562,0.0054080006666481495,0.03592434898018837,0.0001651789789320901,0.017304912209510803,-0.01922907680273056,0.04822206869721413,-0.0688890889286995,0.019858958199620247,-0.0008752745925448835,0.03513675928115845,-0.07729781419038773,0.08145932108163834,-0.0327017717063427,0.03425054997205734,-0.08482713997364044,0.006879036780446768,0.059308722615242004,-0.03618019446730614,-0.056978799402713776,-0.021730659529566765,-0.0007874490693211555,-0.30017349123954773,0.011467894539237022,0.0029629627242684364,-0.00585860526189208,-0.010300826281309128,0.023507587611675262,0.009586751461029053,0.01615791581571102,-0.05407087132334709,-0.0025957857724279165,-0.005770532879978418,0.03627054765820503,0.03723520413041115,0.0002953026269096881,-0.01028500497341156,0.003999052103608847,-0.005846572108566761,0.033623822033405304,-0.0072589460760355,-0.07468357682228088,0.03272583335638046,-0.00448765279725194,0.21248994767665863,-0.057705674320459366,0.044046953320503235,0.03008623979985714,-0.018218697980046272,0.04393533617258072,0.07603447884321213,-0.04150347039103508,0.06695082038640976,-0.010416779667139053,0.08510852605104446,-0.07743050903081894,-0.005964982323348522,0.03540671616792679,-0.036865249276161194,0.058287233114242554,0.005791360046714544,-0.03530560061335564,-0.010620728135108948,0.03216135874390602,0.012065712362527847,-0.05922657623887062,0.08696120232343674,-0.051534030586481094,-0.08612160384654999,-0.04676511138677597,-0.005788259673863649,0.06060168892145157,-0.02552523836493492,-0.02923434041440487,-0.05256013199687004,0.0033684736117720604,0.023232899606227875,0.023369308561086655,-0.02598796784877777,-0.02167469449341297,-0.05872185155749321,-0.0459195151925087,0.008857548236846924,-0.07634632289409637,0.016223475337028503,0.03924580290913582,0.11316763609647751]</span>
</code></pre><h3 id="heading-example-creating-users-via-admin-api">Example: Creating Users via Admin API</h3>
<p>You can also leverage the admin API to create users:</p>
<pre><code class="lang-jsx">select edge.exec(
$js$

<span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.admin.createUser({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'user@email.com'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'password'</span>,
  <span class="hljs-attr">user_metadata</span>: { <span class="hljs-attr">name</span>: <span class="hljs-string">'Yoda'</span> }
});

$js$));
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>As you can see, combining dynamic Javascript in Edge Functions with a few SQL support functions gets you a powerful new set of tools. By leveraging the edge_wrapper, edge.http_request, and <code>edge.exec</code> functions, developers can create robust and flexible serverless applications that can dynamically execute JavaScript code while interacting with PostgreSQL databases.</p>
<p>As we continue to build and innovate with Supabase, combining edge functions and SQL support functions opens up new avenues for building scalable, efficient, and secure applications. Whether developing a simple project or a complex application, these tools provide the flexibility and power to bring your ideas to life.</p>
]]></content:encoded></item><item><title><![CDATA[Unleash Powerful Webhooks with pgwebhook in Supabase]]></title><description><![CDATA[In today's data-driven world, seamless integration between databases and external services is crucial for real-time data synchronization and triggering actions based on database events. While Supabase offers built-in webhooks for Postgres integration...]]></description><link>https://blog.mansueli.com/unleash-powerful-webhooks-with-pgwebhook</link><guid isPermaLink="true">https://blog.mansueli.com/unleash-powerful-webhooks-with-pgwebhook</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[supabase]]></category><category><![CDATA[webhooks]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 25 Jun 2024 18:30:05 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1719340068394/f829061a-e8f8-4d82-860e-78dab7da96fc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In today's data-driven world, seamless integration between databases and external services is crucial for real-time data synchronization and triggering actions based on database events. While Supabase offers built-in webhooks for Postgres integration, managing webhooks can become challenging, especially when dealing with complex scenarios or GDPR compliance. This is where pgwebhook, a powerful Postgres Trusted Language Extension, comes into play. pgwebhook simplifies the integration process by offering robust features like automatic retries, regional fallbacks, and customizable payloads, making it an ideal choice for developers seeking to enhance their <a target="_blank" href="https://supabase.com">Supabase</a> workflows.</p>
<p>This blog post explores the limitations of default Supabase webhooks and how pgwebhook empowers developers to overcome these challenges. We'll delve into the functionalities of pgwebhook and showcase practical use cases that demonstrate its effectiveness.</p>
<h2 id="heading-understanding-the-need-for-integration">Understanding the Need for Integration</h2>
<p>Integrating databases with external services presents several challenges. The default approach for Postgres webhooks with pg_net might fall short when you need to process the results of a call or handle high volumes that could overwhelm the worker process. Additionally, Supabase's built-in webhooks currently lack the ability to customize the payload before sending it to external services.</p>
<h2 id="heading-introducing-pgwebhook-a-revamped-webhook-experience">Introducing pgwebhook: A revamped Webhook experience</h2>
<p><code>pgwebhook</code> is more than just a webhook solution; it's a comprehensive extension designed to streamline Postgres integration with webhooks. Built with reliable technology like cURL (through <a target="_blank" href="https://github.com/pramsey/pgsql-http">pghttp</a>), pgwebhook ensures dependable and performant HTTP requests. It surpasses traditional webhook functionalities by offering features like:</p>
<p><code>Automatic Fallbacks</code>: Ensures uninterrupted operation even during edge function failures. <code>Regional Fallbacks</code>: <em>(exclusive for edge functions)</em> Guarantees compliance with regulations like GDPR by allowing you to restrict calls to specific regions. Customizable Payloads: Provides flexibility in tailoring the data sent to external services. Seamless Supabase Integration: Integrates effortlessly with the Supabase platform, making it an ideal choice for Supabase users.</p>
<h2 id="heading-installation-and-setup">Installation and Setup</h2>
<p>Installing <code>pgwebhook</code> is a straightforward process, whether you're a Supabase user or prefer manual installation.</p>
<p><strong>Installing Database.dev</strong></p>
<pre><code class="lang-sql"><span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> <span class="hljs-keyword">http</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">schema</span> extensions;
<span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> pg_tle;
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span> pgtle.uninstall_extension_if_exists(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">select</span>
    pgtle.install_extension(
        <span class="hljs-string">'supabase-dbdev'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'version'</span>,
        <span class="hljs-string">'PostgreSQL package manager'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'sql'</span>
    )
<span class="hljs-keyword">from</span> <span class="hljs-keyword">http</span>(
    (
        <span class="hljs-string">'GET'</span>,
        <span class="hljs-string">'https://api.database.dev/rest/v1/'</span>
        || <span class="hljs-string">'package_versions?select=sql,version'</span>
        || <span class="hljs-string">'&amp;package_name=eq.supabase-dbdev'</span>
        || <span class="hljs-string">'&amp;order=version.desc'</span>
        || <span class="hljs-string">'&amp;limit=1'</span>,
        <span class="hljs-built_in">array</span>[
            (<span class="hljs-string">'apiKey'</span>, <span class="hljs-string">'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhtdXB0cHBsZnZpaWZyYndtbXR2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODAxMDczNzIsImV4cCI6MTk5NTY4MzM3Mn0.z2CN0mvO2No8wSi46Gw59DFGCTJrzM0AQKsu_5k134s'</span>)::http_header
        ],
        <span class="hljs-literal">null</span>,
        <span class="hljs-literal">null</span>
    )
) x,
<span class="hljs-keyword">lateral</span> (
    <span class="hljs-keyword">select</span>
        ((row_to_json(x) -&gt; <span class="hljs-string">'content'</span>) <span class="hljs-comment">#&gt;&gt; '{}')::json -&gt; 0</span>
) resp(<span class="hljs-keyword">contents</span>);
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span> dbdev.install(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
</code></pre>
<p>Installing pgwebhook:</p>
<pre><code class="lang-sql">
<span class="hljs-comment">-- </span>
<span class="hljs-keyword">SELECT</span> dbdev.install(<span class="hljs-string">'mansueli@pgwebhook'</span>);
<span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-string">"mansueli@pgwebhook"</span> <span class="hljs-keyword">VERSION</span> <span class="hljs-string">'0.1.1'</span>;
</code></pre>
<p>You can also install it manually by running the <a target="_blank" href="https://github.com/mansueli/tle/blob/master/pgwebhook/pgwebhook--0.1.1.sql">SQL script</a> in your database.</p>
<p><a target="_blank" href="https://github.com/mansueli/tle/blob/master/pgwebhook/">https://github.com/mansueli/tle/blob/master/pgwebhook/</a></p>
<h2 id="heading-practical-usage-scenarios">Practical Usage Scenarios</h2>
<p>Let's explore practical scenarios where <code>pgwebhook</code> shines:</p>
<h3 id="heading-direct-usage">Direct Usage:</h3>
<p>Direct calls to external services are sometimes necessary, particularly for edge functions or APIs restricted to specific regions. For instance, imagine OpenAI restricts calls to allowed regions, or for GDPR compliance, you might need to keep your Edge Function calls within specific subregions. In such cases, pgwebhook offers a solution by providing a wrapper function that sets defaults for the integration process.</p>
<h4 id="heading-secure-secret-management-with-vault">Secure Secret Management with Vault</h4>
<p>For best practices, consider storing secrets in Vault and fetching them with functions like:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> vault.get_anon (bearer <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">FALSE</span>)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">TEXT</span> 
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> bearer <span class="hljs-keyword">THEN</span>
        <span class="hljs-keyword">RETURN</span> <span class="hljs-string">'Bearer '</span> || (<span class="hljs-keyword">SELECT</span> decrypted_api_secret <span class="hljs-keyword">FROM</span> secrets.decrypted_api_keys <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'anon'</span>);
    ELSE
        RETURN (<span class="hljs-keyword">SELECT</span> decrypted_api_secret <span class="hljs-keyword">FROM</span> secrets.decrypted_api_keys <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'anon'</span>);
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> vault.get_supabase_url () 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">TEXT</span> 
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">RETURN</span> (<span class="hljs-keyword">SELECT</span> decrypted_api_secret 
            <span class="hljs-keyword">FROM</span> secrets.decrypted_api_keys 
            <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'supabase_url'</span>);
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> 
<span class="hljs-keyword">FUNCTION</span> vault.get_service_role (bearer <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">FALSE</span>) 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">TEXT</span> 
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> bearer <span class="hljs-keyword">THEN</span>
        <span class="hljs-keyword">RETURN</span> <span class="hljs-string">'Bearer '</span> || (<span class="hljs-keyword">SELECT</span> decrypted_api_secret 
                             <span class="hljs-keyword">FROM</span> secrets.decrypted_api_keys 
                             <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'service_role'</span>);    
    ELSE
        RETURN (<span class="hljs-keyword">SELECT</span> decrypted_api_secret 
                <span class="hljs-keyword">FROM</span> secrets.decrypted_api_keys 
                <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'service_role'</span>);
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<p>Following this approach, you can create a wrapper function that ensures calls originate from the specified regions:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create wrapper function in the public schema</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> public.euro_edge (func <span class="hljs-built_in">TEXT</span>, <span class="hljs-keyword">data</span> JSONB) <span class="hljs-keyword">RETURNS</span> JSONB <span class="hljs-keyword">LANGUAGE</span> plpgsql
<span class="hljs-keyword">AS</span> $<span class="hljs-keyword">function</span>$
<span class="hljs-keyword">DECLARE</span>
    custom_headers JSONB;
    allowed_regions TEXT[] := ARRAY['eu-west-1', 
                                    'eu-west-2', 
                                    'eu-west-3', 
                                    'eu-north-1', 
                                    'eu-central-1'];
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Set headers with anon key and Content-Type</span>
    custom_headers := jsonb_build_object(<span class="hljs-string">'Authorization'</span>, 
                                   vault.get_anon_key(bearer := <span class="hljs-literal">true</span>), 
                      <span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'application/json'</span>);
    <span class="hljs-comment">-- Call edge_wrapper function with default values</span>
    RETURN hook.edge_wrapper(url := ('https://supanacho.supabase.co/functions/v1/' || func), 
                             headers := custom_headers, 
                             payload := data, 
                             max_retries := 5, 
                             allowed_regions := allowed_regions);
<span class="hljs-keyword">END</span>;
$function$;
</code></pre>
<h3 id="heading-webhooks-powerful-automation">Webhooks: Powerful Automation</h3>
<p>Webhook triggers are crucial for automating actions based on database events. However, ensuring compliance and reliability can be challenging, especially with edge cases like GDPR or service availability issues. pgwebhook addresses these challenges with features like automatic retries and regional fallbacks.</p>
<p><strong>Using Triggers on a Table</strong></p>
<p>You can use <code>pgwebhook</code> similarly to Supabase webhooks:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> your_trigger_name
<span class="hljs-keyword">AFTER</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">ON</span> your_table
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> hook.webhook_trigger(
    <span class="hljs-string">'https://your-webhook-url.com'</span>,
    <span class="hljs-string">'POST'</span>,
    <span class="hljs-string">'{"Content-Type": "application/json"}'</span>,
    <span class="hljs-string">'{}'</span>,
    <span class="hljs-number">5000</span>
);
</code></pre>
<p><strong>However, you can also get more:</strong></p>
<p><code>pgwebhook</code> goes beyond Supabase webhooks by enabling triggers that automatically handle retries and regional headers. This ensures compliance with regulations like GDPR and maintains service availability during regional outages.</p>
<h4 id="heading-create-a-trigger-using-a-plpgsql-custom-handler">Create a Trigger Using a PL/pgSQL Custom Handler</h4>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> hook.custom_handler_function(payload jsonb)
<span class="hljs-keyword">RETURNS</span> jsonb <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    new_payload jsonb;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Modify the payload as needed</span>
    <span class="hljs-comment">-- Example: Add a new field to the payload</span>
    new_payload := payload || jsonb_build_object(<span class="hljs-string">'additional_info'</span>, <span class="hljs-string">'This is extra info'</span>);

    <span class="hljs-comment">-- Example: Remove a sensitive field</span>
    new_payload := new_payload - 'sensitive_field';

    <span class="hljs-comment">-- Return the modified payload</span>
    RETURN new_payload;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> your_trigger_name
<span class="hljs-keyword">AFTER</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">ON</span> your_table
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> hook.webhook_trigger(
    <span class="hljs-string">'https://your-webhook-url.com'</span>,
    <span class="hljs-string">'POST'</span>,
    <span class="hljs-string">'{"Content-Type": "application/json"}'</span>,
    <span class="hljs-string">'{}'</span>,
    <span class="hljs-number">5000</span>,
    <span class="hljs-string">'hook.custom_handler_function'</span>
);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p><code>pgwebhook</code> is a promising new Postgres Trusted Language Extension that simplifies and streamlines webhook integration for Supabase users. Its robust feature set, including automatic retries, regional fallbacks, and customizable payloads, addresses common challenges associated with managing webhooks effectively. By leveraging <code>pgwebhook</code>, developers can enhance their Supabase workflows and ensure reliable communication between databases and external services.</p>
<h2 id="heading-experimentation-and-community">Experimentation and Community</h2>
<p>As an actively developed project, pgwebhook welcomes contributions from the community. We encourage you to explore the possibilities of pgwebhook and share your feedback. Feel free to experiment, submit pull requests (PRs) for improvements, and open issues on GitHub to report any bugs or request new features.</p>
<p>We believe that pgwebhook has the potential to become a valuable asset for the Supabase developer community. Join us on GitHub (https://github.com/mansueli/tle/blob/master/pgwebhook/) to explore further and contribute to its development!</p>
]]></content:encoded></item><item><title><![CDATA[Exploring Support Tooling at Supabase: A Dive into SLA Buddy]]></title><description><![CDATA[Introduction
In database management and support operations, ensuring Service Level Agreement (SLA) compliance is paramount. Supabase, known for its innovative approach to database management and support, introduces SLA Buddy, a robust support tool ai...]]></description><link>https://blog.mansueli.com/exploring-support-tooling-at-supabase-a-dive-into-sla-buddy</link><guid isPermaLink="true">https://blog.mansueli.com/exploring-support-tooling-at-supabase-a-dive-into-sla-buddy</guid><category><![CDATA[slack-bot]]></category><category><![CDATA[slack]]></category><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[sla]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Fri, 10 May 2024 11:57:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714082151140/e0134e37-5e10-471b-b8b3-65e78eb07f59.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>In database management and support operations, ensuring Service Level Agreement (SLA) compliance is paramount. Supabase, known for its innovative approach to database management and support, introduces SLA Buddy, a robust support tool aimed at efficient SLA enforcement. This blog post delves into the intricacies of SLA Buddy, shedding light on its functions, operations, and interactions within the Supabase ecosystem.</p>
<h3 id="heading-introducing-sla-buddy">Introducing SLA Buddy</h3>
<p>Supabase's commitment to innovation extends beyond database solutions; it encompasses robust support operations. SLA Buddy stands as a testament to Supabase's dedication to streamlining support processes and ensuring timely resolution of user queries.</p>
<h2 id="heading-dogfooding-the-birth-of-sla-buddy">Dogfooding: The Birth of SLA Buddy</h2>
<p>Supabase firmly believes in dogfooding a philosophy that entails using one's own products internally. This approach played a pivotal role in the creation of SLA Buddy. Leveraging Supabase's suite of tools, including Edge Functions and Database functionalities, SLA Buddy was meticulously developed to meet the stringent demands of support operations.</p>
<h2 id="heading-understanding-sla-buddys-functions">Understanding SLA Buddy's Functions</h2>
<p>SLA Buddy's core function revolves around enforcing SLAs effectively. Let's delve into its primary functions:</p>
<h3 id="heading-sla-enforcement">SLA Enforcement</h3>
<p>SLA Buddy ensures SLA compliance through a series of intricate processes. This includes:</p>
<ul>
<li><p><strong>Slack Reminders</strong>: Utilizing Slack reminders to prompt support engineers about impending SLA deadlines.</p>
</li>
<li><p><strong>Calendar Checks</strong>: Employing calendar integration to determine who's currently available to answer support tickets.</p>
</li>
</ul>
<h2 id="heading-lets-take-a-look-at-sla-buddys-operations">Let's take a look at SLA Buddy's Operations</h2>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714082503587/a97bda9f-197a-4971-8b2a-0670060fe24c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-watching-messages">Watching Messages</h3>
<p>SLA Buddy actively monitors Slack channels using PostgreSQL functions like <code>process_channels</code>. This function scans Slack channels, handles new messages, and adds tasks to the queue for each new ticket that comes to the platform. Once the channel is scanned through the scan_channel edge function it adds rows to the <code>slack_watcher</code> table. There is a trigger function on that table that creates tasks for each ticket according to the SLA which depends on which channel that the message came from. Tickets have different SLAs, depending on both severity and the subscription level of the user opening the ticket.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"insert_tasks"</span>() <span class="hljs-keyword">RETURNS</span> <span class="hljs-string">"trigger"</span>
    <span class="hljs-keyword">LANGUAGE</span> <span class="hljs-string">"plpgsql"</span>
    <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">declare</span>
    escalationtimeintervals <span class="hljs-built_in">int</span>[];
    currentinterval int;
    threadts text;

<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">IF</span> new.channel_id &lt;&gt; <span class="hljs-string">''</span> <span class="hljs-keyword">THEN</span>
        <span class="hljs-keyword">SELECT</span> escalation_time <span class="hljs-keyword">INTO</span> escalationtimeintervals
          <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">priority</span> <span class="hljs-keyword">WHERE</span> channel_id = new.channel_id;
    ELSE
        escalationtimeintervals := array[10, 20, 35, 50]; <span class="hljs-comment">-- minutes</span>
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
    <span class="hljs-comment">-- INSERT tasks for each escalation level</span>
    FOR i IN 1..4
    LOOP
        <span class="hljs-comment">-- set the current escalation time interval</span>
        currentinterval := escalationtimeintervals[i];
        <span class="hljs-comment">-- format thread_ts as (epoch time as a big int) + '.' + ts_ms</span>
        thread_timestamp := extract(epoch FROM new.ts)::bigint::text || '.' || new.ts_ms;

        <span class="hljs-comment">-- check IF ticket_type is not 'feedback'</span>
        IF lower(new.ticket_type) &lt;&gt; 'feedback' THEN
            <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> checking_tasks_queue (http_verb, payload, due_time, replied)
            <span class="hljs-keyword">values</span> (
                <span class="hljs-string">'POST'</span>,
                jsonb_build_object(
                    <span class="hljs-string">'channel_id'</span>, new.channel_id,
                    <span class="hljs-string">'thread_ts'</span>, thread_timestamp,
                    <span class="hljs-string">'escalation_level'</span>, i,
                    <span class="hljs-string">'ticket_id'</span>, new.ticket_number,
                    <span class="hljs-string">'ticket_priority'</span>, new.ticket_priority,
                    <span class="hljs-string">'ticket_type'</span>, new.ticket_type
                ),
                new.ts + (currentinterval * <span class="hljs-built_in">interval</span> <span class="hljs-string">'1 minute'</span>),
                <span class="hljs-literal">false</span>
            );
        <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;
    <span class="hljs-comment">-- return the new slack_msg row</span>
    return new;
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<h3 id="heading-verifying-due-tasks"><strong>Verifying Due Tasks</strong></h3>
<p>The core function <code>check_due_tasks_and_update()</code> plays a pivotal role in task verification and status updating. It ensures that tasks are duly acknowledged, thereby facilitating timely resolution.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"check_due_tasks_and_update"</span>() <span class="hljs-keyword">RETURNS</span> <span class="hljs-string">"void"</span>
    <span class="hljs-keyword">LANGUAGE</span> <span class="hljs-string">"plpgsql"</span>
    <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    _task <span class="hljs-built_in">RECORD</span>;
    _response JSONB;
    _response_row JSONB;
    _ticket_id text;
    _have_replied BOOLEAN;
    _ticket_array text;
    _lock_key CONSTANT int := 42;
    _lock_acquired boolean;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Try to acquire the advisory lock</span>
    _lock_acquired := pg_try_advisory_lock(_lock_key);
    IF NOT _lock_acquired THEN
        RAISE NOTICE 'Could not acquire lock. Another instance is running. Exiting function...';
        RETURN;
    END IF;

    <span class="hljs-comment">-- Call create_ticket_array()</span>
    RAISE NOTICE 'Calling create_ticket_array()';
    _ticket_array := public.create_ticket_array();

    <span class="hljs-comment">-- Check IF _ticket_array is '[]'</span>
    IF _ticket_array = '[]' THEN
        RAISE NOTICE 'No tickets to process. Exiting function...';
        <span class="hljs-comment">-- Release the advisory lock</span>
        PERFORM pg_advisory_unlock(_lock_key);
        RETURN;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Call help_plataform_wrapper() using _ticket_array</span>
    RAISE NOTICE 'Calling help_plataform_wrapper()';
    _response := public.help_plataform_wrapper(_ticket_array);

    <span class="hljs-comment">-- Check IF _response is NULL</span>
    IF _response IS NULL THEN
        RAISE NOTICE 'Response is NULL. Exiting function...';
        <span class="hljs-comment">-- Release the advisory lock</span>
        PERFORM pg_advisory_unlock(_lock_key);
        RETURN;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

    <span class="hljs-comment">-- Process the response</span>
    FOR _response_row IN <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> jsonb_array_elements(_response)
    <span class="hljs-keyword">LOOP</span>
        _ticket_id := _response_row-&gt;&gt;<span class="hljs-string">'ticket_id'</span>;
        _have_replied := (_response_row-&gt;&gt;'have_replied')::BOOLEAN;
        RAISE NOTICE 'Processing response for ticket_id: %, have_replied: %', _ticket_id, _have_replied;
        IF _have_replied THEN
            RAISE NOTICE 'Ticket % has a reply. Updating...', _ticket_id;
            <span class="hljs-comment">-- Perform actions for replied tickets</span>
            <span class="hljs-keyword">UPDATE</span> public.checking_tasks_queue
            <span class="hljs-keyword">SET</span> replied_at = <span class="hljs-keyword">NOW</span>(), replied = <span class="hljs-literal">TRUE</span>
            <span class="hljs-keyword">WHERE</span> payload-&gt;&gt;<span class="hljs-string">'ticket_id'</span> = _ticket_id;
        ELSE
            RAISE NOTICE 'Ticket % has no reply. Taking actions...', _ticket_id;
            <span class="hljs-comment">-- Perform actions for no reply</span>
            <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">INTO</span> _task <span class="hljs-keyword">FROM</span> public.checking_tasks_queue
            <span class="hljs-keyword">WHERE</span> payload-&gt;&gt;<span class="hljs-string">'ticket_id'</span> = _ticket_id <span class="hljs-keyword">AND</span> <span class="hljs-keyword">status</span> = <span class="hljs-string">''</span> <span class="hljs-keyword">AND</span> due_time &lt;= <span class="hljs-keyword">NOW</span>()
            <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> due_time <span class="hljs-keyword">ASC</span>
            <span class="hljs-keyword">LIMIT</span> <span class="hljs-number">1</span>;

            IF FOUND THEN
                RAISE NOTICE 'Sending Slack notification for ticket %', _ticket_id;
                <span class="hljs-comment">-- Use EXCEPTION to handle duplicate keys</span>
                <span class="hljs-keyword">BEGIN</span>
                    <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> post_to_slack_log(payload) <span class="hljs-keyword">VALUES</span> (_task.payload);
                    PERFORM slack_post_wrapper(_task.payload);
                EXCEPTION
                    WHEN unique_violation THEN
                        RAISE NOTICE 'Duplicate entry for ticket %. Skipping...', _ticket_id;
                    WHEN OTHERS THEN
                        RAISE NOTICE 'Error while inserting into post_to_slack_log. Skipping...';
                        RAISE NOTICE '% %', SQLERRM, SQLSTATE;
                <span class="hljs-keyword">END</span>;
                <span class="hljs-comment">-- Update the status to 'sent' after calling slack_post_wrapper</span>
                <span class="hljs-keyword">UPDATE</span> public.checking_tasks_queue
                <span class="hljs-keyword">SET</span> <span class="hljs-keyword">status</span> = <span class="hljs-string">'sent'</span>
                <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = _task.id;
            ELSE
                RAISE NOTICE 'Task for ticket % not found!', _ticket_id;
            <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
        <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
    <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;
    <span class="hljs-comment">-- Release the advisory lock</span>
    PERFORM pg_advisory_unlock(_lock_key);
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<h3 id="heading-posting-sla-enforcement-messages-on-slack">Posting SLA Enforcement Messages on Slack</h3>
<p>SLA Buddy employs the Edge Function <code>post_ticket_escalation</code> to post SLA enforcement messages on Slack. This integration with PostgreSQL functions ensures streamlined execution and effective communication with support engineers.</p>
<h2 id="heading-interactions-with-support-members">Interactions with Support Members</h2>
<p>SLA Buddy fosters seamless interactions between support engineers and the tool itself. Through Slack threads, support members can postpone the next steps in the escalation process by 30 min by <code>@mentioning</code> the bot in the thread. We also pushed a <a target="_blank" href="https://supabase.com/docs/guides/functions/examples/slack-bot-mention">guide on how to interact with mentions</a> in Slack as part of the bot's development.</p>
<p>The bot won't get disarmed until a response is sent in the ticket because we believe that even if the Support Engineer is unable to help the user, they can at least triage and set expectations for the next steps in the ticket like escalating to a specific team.</p>
<h2 id="heading-watching-support-events">Watching Support Events</h2>
<p>Another crucial aspect of SLA Buddy is its ability to monitor support events seamlessly. At Supabase we have the concept of Embedded Support when a member of the support team will work on more advanced tickets related to a specific Supabase product such as Edge Functions, Dashboard, Storage, Auth, Realtime etc.</p>
<p>The shift information about Support Engineers is hosted in a Google Calendar. This information is retrieved using the following function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> <span class="hljs-string">"public"</span>.<span class="hljs-string">"get_embedded_event_names"</span>
    (<span class="hljs-string">"date_param"</span> <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">"now"</span>())
  <span class="hljs-keyword">RETURNS</span> <span class="hljs-string">"jsonb"</span>
  <span class="hljs-keyword">LANGUAGE</span> <span class="hljs-string">"plpgsql"</span> <span class="hljs-keyword">SECURITY</span> DEFINER
  <span class="hljs-keyword">SET</span> <span class="hljs-string">"search_path"</span> <span class="hljs-keyword">TO</span> <span class="hljs-string">'public'</span>, <span class="hljs-string">'extensions'</span>, <span class="hljs-string">'vault'</span>
  <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  target_date <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone := <span class="hljs-keyword">COALESCE</span>(date_param, <span class="hljs-keyword">now</span>());
  start_date timestamp <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone := target_date + <span class="hljs-built_in">INTERVAL</span> <span class="hljs-string">'2 hours'</span>;
  end_date timestamp <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone := start_date + <span class="hljs-built_in">INTERVAL</span> <span class="hljs-string">'1 day'</span> - <span class="hljs-built_in">INTERVAL</span> <span class="hljs-string">'1 millisecond'</span>;
  time_min text := to_char(start_date, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"');
  time_max text := to_char(end_date, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"');
  base_url text;
  api_url text;
  response jsonb;
  events jsonb; <span class="hljs-comment">-- Change the declaration to jsonb</span>
  embedded_event_names text[];
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">SELECT</span> decrypted_secret
  <span class="hljs-keyword">INTO</span> base_url
  <span class="hljs-keyword">FROM</span> vault.decrypted_secrets
  <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'calendar_base_url'</span>;

  api_url := base_url || '&amp;timeMin=' || time_min || '&amp;timeMax=' || time_max;

  <span class="hljs-keyword">select</span> <span class="hljs-string">"content"</span>::jsonb <span class="hljs-keyword">into</span> response <span class="hljs-keyword">from</span> http_get(api_url);
  events := response-&gt;'items'; <span class="hljs-comment">-- Remove the typecast to ::jsonb</span>

  <span class="hljs-keyword">SELECT</span> ARRAY_AGG(<span class="hljs-keyword">event</span>-&gt;&gt;<span class="hljs-string">'summary'</span>)
  <span class="hljs-keyword">INTO</span> embedded_event_names
  <span class="hljs-keyword">FROM</span> jsonb_array_elements(<span class="hljs-keyword">events</span>) <span class="hljs-keyword">AS</span> <span class="hljs-keyword">event</span> <span class="hljs-comment">-- Use jsonb_array_elements function</span>
  <span class="hljs-keyword">WHERE</span> (<span class="hljs-keyword">event</span>-&gt;&gt;<span class="hljs-string">'summary'</span>) <span class="hljs-keyword">ILIKE</span> <span class="hljs-string">'%embedded%'</span>;
  RETURN COALESCE(to_jsonb(embedded_event_names)::text,'[]');
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<p><strong>Escalation Logic</strong></p>
<p>SLA Buddy's escalation logic is defined in 4 steps of escalation going from a more narrow set of Support Engineers to the Head of Success. Here's the progression:</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Target</td><td>Level</td><td>Action</td><td>Timeline</td></tr>
</thead>
<tbody>
<tr>
<td>Enterprise</td><td>1</td><td>Non-embedded support</td><td>10 min</td></tr>
<tr>
<td></td><td>2</td><td>On-shift support</td><td>20 min</td></tr>
<tr>
<td></td><td>3</td><td>@group-support</td><td>35 min</td></tr>
<tr>
<td></td><td>4</td><td>@head of success</td><td>50 min</td></tr>
<tr>
<td>Teams</td><td>1</td><td>Non-embedded support</td><td>1 hour</td></tr>
<tr>
<td></td><td>2</td><td>On-shift support</td><td>3 hours</td></tr>
<tr>
<td></td><td>3</td><td>@group-support</td><td>6 hours</td></tr>
<tr>
<td></td><td>4</td><td>@head of success</td><td>12 hours</td></tr>
</tbody>
</table>
</div><h2 id="heading-conclusion">Conclusion</h2>
<p>SLA Buddy is a core operational component for Supabase support operations, keeping the whole team informed and engaged, and assisting with prioritizing tickets by their SLA restrictions.</p>
<p>We are firm believers in letting technology streamline operational work and allowing humans to focus on solving real problems, and SLA Buddy is a great example of that.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>SLA Buddy started a passion project, born from a need to ensure that we're providing top-quality support to Supabase's users. We're big fans of personal exploration and kaizen incremental change.</p>
<p>And we're not done with SLA Buddy. It'll grow and evolve as Supabase grows, and our needs and the needs of our users change. Because it's built on Supabase features, it'll be easy to update and maintain, and it'll provide more and more value to our internal operations, we hope it might provide some value to you, too. We're also big believers in the Open Source community, and welcome any feedback or ideas you might have to make SLA Buddy even better for everyone.</p>
<h2 id="heading-more-resources-about-slack-and-edge-functions">More Resources About Slack and Edge Functions</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/mansueli/slabuddy">GitHub Repo: SLA Buddy</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/functions/examples/slack-bot-mention">Docs Edge Functions: slack mention reply</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/functions/getting-started">Docs Edge Functions: geting started</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/functions/debugging">Docs Edge Functions: debugging</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/blog/slack-consolidate-slackbot-to-consolidate-messages">Slack Consolidate: a slackbot build with Python &amp; Supabase</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building a Slack Bot for AI-Powered Conversations with Supabase]]></title><description><![CDATA[In today's tech-driven world, integrating AI into your communication tools can enhance productivity and user experiences. Slack, a widely used team collaboration platform, allows developers to create custom bots that automate tasks and provide intell...]]></description><link>https://blog.mansueli.com/ai-powered-slack-bot-supabase</link><guid isPermaLink="true">https://blog.mansueli.com/ai-powered-slack-bot-supabase</guid><category><![CDATA[slack]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Edge-Functions]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[AI]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 10 Oct 2023 13:25:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696801001502/df0087a5-9ed1-4ce2-8804-e3fb1dfb7df7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In today's tech-driven world, integrating AI into your communication tools can enhance productivity and user experiences. Slack, a widely used team collaboration platform, allows developers to create custom bots that automate tasks and provide intelligent responses. In this blog post, we'll guide you through the process of building a Slack bot that harnesses the power of AI, specifically Hugging Face and OpenAI, using the Supabase platform.</p>
<p><strong>Prerequisites</strong>:</p>
<p>Before we dive into the code, you'll need a few things:</p>
<ol>
<li><p><strong>Supabase Account</strong>: Ensure you have an account on <a target="_blank" href="http://supabase.com/">Supabase</a>.</p>
</li>
<li><p><strong>Slack Workspace</strong>: You'll require access to a Slack workspace where you can create and install the bot.</p>
</li>
<li><p><strong>API Keys</strong>: Obtain API keys for Hugging Face and OpenAI.</p>
</li>
</ol>
<p><strong>Creating the Slack Bot Manifest</strong>:</p>
<p>To begin, you need to define the bot's characteristics in a manifest file (<code>manifest.yaml</code>). This file specifies the bot's display name, features, OAuth configuration, and settings. Here's an example manifest for your Slack AI bot:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">display_information:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">SlackAI</span>
<span class="hljs-attr">features:</span>
  <span class="hljs-attr">bot_user:</span>
    <span class="hljs-attr">display_name:</span> <span class="hljs-string">SlackAI</span>
    <span class="hljs-attr">always_online:</span> <span class="hljs-literal">false</span>
<span class="hljs-attr">oauth_config:</span>
  <span class="hljs-attr">scopes:</span>
    <span class="hljs-attr">bot:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">app_mentions:read</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">chat:write</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">im:write</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">channels:history</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">commands</span>
<span class="hljs-attr">settings:</span>
  <span class="hljs-attr">org_deploy_enabled:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">socket_mode_enabled:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">token_rotation_enabled:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>We'll interact with the Slack bot using custom webhooks. For detailed instructions, refer to our previous post on <a target="_blank" href="https://blog.mansueli.com/automating-webhooks-supabase-postgresql-guide">Deploying and Debugging Custom Webhooks on Supabase &amp; PostgreSQL</a>.</p>
<p><strong>Setting Secrets on Supabase CLI</strong>:</p>
<p>You'll need to securely store sensitive information, such as API keys, on Supabase using the CLI. Here's how to do it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Set OpenAI API Key</span>
supabase --project-ref nacho_slacker secrets \
<span class="hljs-built_in">set</span> OPEN_AI=sk-FB9ZJJbluyN4CH0luSQkwG0t6ZZG3wSQ6SFyBj3k8JZDRKSt

<span class="hljs-comment"># Set Hugging Face API Key</span>
supabase --project-ref nacho_slacker secrets \
<span class="hljs-built_in">set</span> HUGGINGFACE_TOKEN=hf_rSji7SZZuZwN8ZW53tN4CH0TKRtFCN6Cai

<span class="hljs-comment"># Set Bot OAuth Token</span>
supabase --project-ref nacho_slacker secrets \
<span class="hljs-built_in">set</span> SLACK_TOKEN=xoxb-3882900064320-6000640064577-QDr8Nk637kw6nachoR7bDu9
</code></pre>
<h3 id="heading-edge-functions-the-soul-of-your-slack-bot"><strong>Edge Functions: The Soul of Your Slack Bot</strong></h3>
<p>In this section, we'll explore two critical Edge Functions, namely <code>slack_ai_mentions.ts</code> and <code>consume_job.ts</code>. These functions play vital roles in your Slack bot's functionality.</p>
<ol>
<li><code>slack_ai_mentions.ts</code>: This Edge Function sets up an HTTP server using Deno to listen for Slack events. When the bot receives an app mention, it processes the text adding it into a Queue System in Postgres, which will process the requests and send them back to the user.</li>
</ol>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/std@0.197.0/http/server.ts'</span>;
<span class="hljs-keyword">import</span> { WebClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/x/slack_web_api@6.7.2/mod.js'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2'</span>;

<span class="hljs-keyword">const</span> slack_bot_token = Deno.env.get(<span class="hljs-string">"SLACK_TOKEN"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> bot_client = <span class="hljs-keyword">new</span> WebClient(slack_bot_token);
<span class="hljs-keyword">const</span> supabase_url = Deno.env.get(<span class="hljs-string">"SUPABASE_URL"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> service_role = Deno.env.get(<span class="hljs-string">"SUPABASE_SERVICE_ROLE_KEY"</span>);
<span class="hljs-keyword">const</span> supabase = <span class="hljs-keyword">new</span> SupabaseClient(supabase_url, service_role);

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Slack URL verification function up and running!`</span>);

serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> req_body = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify(req_body, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));
    <span class="hljs-keyword">const</span> { token, challenge, <span class="hljs-keyword">type</span>, event } = req_body;

    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> == <span class="hljs-string">'url_verification'</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ challenge }), {
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        status: <span class="hljs-number">200</span>,
      });
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (event.type == <span class="hljs-string">'app_mention'</span>) {
      <span class="hljs-keyword">const</span> { user, text, channel, ts } = event;
      <span class="hljs-keyword">const</span> url_path = text.toLowerCase()
            .includes(<span class="hljs-string">'code'</span>) ? <span class="hljs-string">'/code'</span> : <span class="hljs-string">'/general'</span>;
      <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">'job_queue'</span>).insert({
        http_verb: <span class="hljs-string">'POST'</span>,
        payload: { user, text, channel, ts },
        url_path: url_path
      });

      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-built_in">console</span>.error(error);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: error.message }), {
          headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
          status: <span class="hljs-number">400</span>,
        });
      }
      <span class="hljs-keyword">await</span> post(channel, 
                 ts, 
                 <span class="hljs-string">`Taking a look and will get back to you shortly!`</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">''</span>, { status: <span class="hljs-number">200</span> });
    }
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: error.message }), {
      headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
      status: <span class="hljs-number">400</span>,
    });
  }
});

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">channel: <span class="hljs-built_in">string</span>, thread_ts: <span class="hljs-built_in">string</span>, message: <span class="hljs-built_in">string</span></span>): 
  <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> bot_client.chat.postMessage({
      channel: channel,
      thread_ts: thread_ts,
      text: message,
    });
    <span class="hljs-built_in">console</span>.info(result);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Error posting message: <span class="hljs-subst">${e}</span>`</span>);
  }
}
</code></pre>
<ol>
<li><code>consume_job.ts</code>: This Edge Function manages tasks related to AI model interactions. It handles requests from the slack_ai_mentions.ts file and communicates with either Hugging Face or OpenAI to generate text responses. The blog post provides detailed instructions on installing and configuring this Edge Function.</li>
</ol>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/std@0.197.0/http/server.ts"</span>;
<span class="hljs-keyword">import</span> { WebClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/x/slack_web_api@6.7.2/mod.js"</span>;

<span class="hljs-keyword">const</span> slack_bot_token = Deno.env.get(<span class="hljs-string">"SLACK_TOKEN"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> bot_client = <span class="hljs-keyword">new</span> WebClient(slack_bot_token);
<span class="hljs-keyword">const</span> hf_token = Deno.env.get(<span class="hljs-string">"HUGGINGFACE_TOKEN"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> openai_api_key = Deno.env.get(<span class="hljs-string">"OPEN_AI"</span>) ?? <span class="hljs-string">""</span>;

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Function that will handle the tasks!"</span>);

serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">const</span> payload = <span class="hljs-keyword">await</span> req.json();
  <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(req.url);
  <span class="hljs-keyword">const</span> method = req.method;

  <span class="hljs-comment">// Extract the last part of the path as the command</span>
  <span class="hljs-keyword">const</span> command = url.pathname.split(<span class="hljs-string">"/"</span>).pop();
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">let</span> generated_text = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">if</span> (command == <span class="hljs-string">"general"</span>) {
      generated_text = <span class="hljs-keyword">await</span> getGeneratedTextFromHuggingFace(payload);
    } <span class="hljs-keyword">else</span> {
      generated_text = <span class="hljs-keyword">await</span> getGeneratedTextFromChatGPT(payload);
    }
    <span class="hljs-keyword">await</span> post(payload.channel, 
               payload.ts, 
               <span class="hljs-string">`Thanks for asking: <span class="hljs-subst">${generated_text}</span>`</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>,
      { headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> }, 
        status: <span class="hljs-number">200</span>
      },
    );
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(error);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(
      <span class="hljs-built_in">JSON</span>.stringify({ error: error.message }),
      {
        headers: { <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
        status: <span class="hljs-number">500</span>,
      },
    );
  }
});

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">
  channel: <span class="hljs-built_in">string</span>,
  thread_ts: <span class="hljs-built_in">string</span>,
  message: <span class="hljs-built_in">string</span>,
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">void</span>&gt; </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> bot_client.chat.postMessage({
      channel: channel,
      thread_ts: thread_ts,
      text: message,
    });
    <span class="hljs-built_in">console</span>.info(result);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Error posting message: <span class="hljs-subst">${e}</span>`</span>);
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getGeneratedTextFromHuggingFace</span>(<span class="hljs-params">payload</span>) </span>{
  <span class="hljs-keyword">let</span> huggingface_url = <span class="hljs-string">""</span>;
  <span class="hljs-keyword">let</span> body_content = <span class="hljs-string">""</span>;
  huggingface_url = <span class="hljs-string">"https://api-inference.huggingface.co/models/mistralai/Mistral-7B-Instruct-v0.1"</span>;
  body_content = <span class="hljs-string">`[INST] <span class="hljs-subst">${payload.text}</span> [/INST]`</span>;
  body_content = <span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-string">"inputs"</span>: body_content }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>);

  <span class="hljs-keyword">const</span> huggingface_response = <span class="hljs-keyword">await</span> fetch(huggingface_url, {
    method: <span class="hljs-string">"POST"</span>,
    headers: {
      <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${hf_token}</span>`</span>,
    },
    body: body_content,
  });
  <span class="hljs-keyword">const</span> huggingface_data = <span class="hljs-keyword">await</span> huggingface_response.json();
  <span class="hljs-keyword">if</span> (huggingface_data.error) {
    <span class="hljs-built_in">console</span>.error(huggingface_data.error);
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(huggingface_data.error.message);
  }
  <span class="hljs-keyword">const</span> generated_text = huggingface_data[<span class="hljs-number">0</span>]
        .generated_text.split(<span class="hljs-string">"[/INST]"</span>);
  <span class="hljs-keyword">return</span> generated_text.length &gt; <span class="hljs-number">1</span> ? 
         generated_text[<span class="hljs-number">1</span>] : generated_text[<span class="hljs-number">0</span>];
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getGeneratedTextFromChatGPT</span>(<span class="hljs-params">payload</span>) </span>{
  <span class="hljs-keyword">const</span> headers = {
    <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
    <span class="hljs-string">"Authorization"</span>: <span class="hljs-string">`Bearer <span class="hljs-subst">${openai_api_key}</span>`</span>,
  };

  <span class="hljs-keyword">const</span> openai_url = <span class="hljs-string">"https://api.openai.com/v1/chat/completions"</span>;

  <span class="hljs-keyword">let</span> body_content = {
    <span class="hljs-string">"model"</span>: <span class="hljs-string">"gpt-3.5-turbo"</span>, <span class="hljs-comment">// Replace with the name of the chat model you want to use</span>
    <span class="hljs-string">"messages"</span>: [
      {
        <span class="hljs-string">"role"</span>: <span class="hljs-string">"system"</span>,
        <span class="hljs-string">"content"</span>: <span class="hljs-string">`You are a detail-oriented, helpful, 
                    and eager to please assistant.`</span>
      },
      {
        <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
        <span class="hljs-string">"content"</span>: payload.text
      }
    ]
  };
  <span class="hljs-keyword">const</span> body_content_text = <span class="hljs-built_in">JSON</span>.stringify(body_content, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>);
  <span class="hljs-keyword">const</span> openai_response = <span class="hljs-keyword">await</span> fetch(openai_url, {
    method: <span class="hljs-string">"POST"</span>,
    headers: headers,
    body: body_content_text,
  });
  <span class="hljs-keyword">const</span> openai_data = <span class="hljs-keyword">await</span> openai_response.json();
  <span class="hljs-keyword">if</span> (openai_data.error) {
    <span class="hljs-built_in">console</span>.error(openai_data.error);
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(openai_data.error.message);
  }
  <span class="hljs-keyword">return</span> openai_data.choices[<span class="hljs-number">0</span>].message.content.trim();
}
</code></pre>
<p>These Edge Functions are essential components of the Slack bot, allowing it to seamlessly integrate with external AI services and provide intelligent responses in real-time.</p>
<p><strong>Deploying Edge Functions</strong>:</p>
<p>Next, you'll deploy these two Edge Functions using Supabase CLI:</p>
<pre><code class="lang-bash">supabase <span class="hljs-built_in">functions</span> deploy consume_job \
--project-ref nacho_slacker --no-verify-jwt

supabase <span class="hljs-built_in">functions</span> deploy slack_ai_mentions \
--project-ref nacho_slacker --no-verify-jwt
</code></pre>
<h2 id="heading-storing-secrets-securely-in-vault">Storing Secrets Securely in Vault</h2>
<p>To ensure the security of sensitive information, it's crucial to utilize Vault for secret management. Here's a step-by-step guide to adding <code>service_role</code> and <code>consumer_function</code> to Vault:</p>
<ol>
<li><p>Add <code>service_role</code> by using the <code>service_role</code> key found in the <a target="_blank" href="https://supabase.green/dashboard/project/_/settings/api">Supabase dashboard</a>.</p>
</li>
<li><p>Incorporate <code>consumer_function</code> into Vault by utilizing the URL of the Edge Function <code>consume_job</code> from the <a target="_blank" href="https://supabase.green/dashboard/project/_/functions">Supabase dashboard</a>.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696800578158/5cae0a7f-5fdc-4c49-9aca-6f4727ac3d42.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<h2 id="heading-configuring-slack-apps">Configuring Slack Apps</h2>
<p>For your bot to seamlessly interact with Slack, you'll need to configure Slack Apps:</p>
<ol>
<li><p>Navigate to the Slack Apps page.</p>
</li>
<li><p>Under "Event Subscriptions," add the URL of the <code>slack_ai_mentions</code> function and click to verify the URL.</p>
</li>
<li><p>The Edge function will respond, confirming that everything is set up correctly.</p>
</li>
<li><p>Add <code>app-mention</code> in the events the bot will subscribe to.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1696800471362/73b86a1d-3b7a-4014-9411-493ef023a947.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-installing-dbdev-and-supaqueue">Installing DBDEV and Supa_Queue</h2>
<p>To enhance your Supabase project's capabilities, consider installing the <code>supabase-dbdev</code> and <code>supa_queue</code> extensions. Here's how you can do it:</p>
<h3 id="heading-installing-supabase-dbdev">Installing <code>supabase-dbdev</code></h3>
<pre><code class="lang-sql"><span class="hljs-comment">-- Install supabase-dbdev</span>
<span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> <span class="hljs-keyword">http</span> <span class="hljs-keyword">with</span> <span class="hljs-keyword">schema</span> extensions;
<span class="hljs-keyword">create</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">exists</span> pg_tle;
<span class="hljs-keyword">select</span> pgtle.uninstall_extension_if_exists(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span>
    pgtle.install_extension(
        <span class="hljs-string">'supabase-dbdev'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'version'</span>,
        <span class="hljs-string">'PostgreSQL package manager'</span>,
        resp.contents -&gt;&gt; <span class="hljs-string">'sql'</span>
    )
<span class="hljs-keyword">from</span> <span class="hljs-keyword">http</span>(
    (
        <span class="hljs-string">'GET'</span>,
        <span class="hljs-string">'https://api.database.dev/rest/v1/'</span>
        || <span class="hljs-string">'package_versions?select=sql,version'</span>
        || <span class="hljs-string">'&amp;package_name=eq.supabase-dbdev'</span>
        || <span class="hljs-string">'&amp;order=version.desc'</span>
        || <span class="hljs-string">'&amp;limit=1'</span>,
        <span class="hljs-built_in">array</span>[
            (<span class="hljs-string">'apiKey'</span>, <span class="hljs-string">'YOUR_DATABASE_DEV_API_KEY_HERE'</span>)::http_header
        ],
        <span class="hljs-literal">null</span>,
        <span class="hljs-literal">null</span>
    )
) x,
<span class="hljs-keyword">lateral</span> (
    <span class="hljs-keyword">select</span>
        ((row_to_json(x) -&gt; <span class="hljs-string">'content'</span>) <span class="hljs-comment">#&gt;&gt; '{}')::json -&gt; 0</span>
) resp(<span class="hljs-keyword">contents</span>);
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">select</span> dbdev.install(<span class="hljs-string">'supabase-dbdev'</span>);
<span class="hljs-keyword">drop</span> extension <span class="hljs-keyword">if</span> <span class="hljs-keyword">exists</span> <span class="hljs-string">"supabase-dbdev"</span>;
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"supabase-dbdev"</span>;
</code></pre>
<h3 id="heading-installing-the-supaqueue-extension">Installing the <code>supa_queue</code> Extension</h3>
<p>To further empower your Supabase project, consider installing the <code>supa_queue</code> extension. This extension offers a robust queue system for efficiently managing tasks and processes. Here's how you can install it:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Install supa_queue</span>
<span class="hljs-keyword">select</span> dbdev.install(<span class="hljs-string">'mansueli-supa_queue'</span>);
<span class="hljs-keyword">create</span> extension <span class="hljs-string">"mansueli-supa_queue"</span> <span class="hljs-keyword">version</span> <span class="hljs-string">'1.0.3'</span>;
</code></pre>
<p><strong>Note</strong>: The <code>supa_queue</code> extension is a Trusted Language Extension available at <a target="_blank" href="https://database.dev/mansueli/supa_queue">https://database.dev/mansueli/supa_queue</a>. It was created based on a previous post we published about <a target="_blank" href="https://blog.mansueli.com/building-a-queue-system-with-supabase-and-postgresql">building a simple &amp; robust queue system for PostgreSQL</a>. This extension enables you to implement a powerful queue system in your Supabase project, which can prove invaluable for managing asynchronous tasks and background processing.</p>
<h2 id="heading-installing-and-interacting-with-your-slack-bot">Installing and Interacting with Your Slack Bot</h2>
<p>Now that your Slack bot is ready to roll, it's time to integrate it into your Slack workspace and start interacting with it in two different contexts: one for general use with the Hugging Face model and another for code-related inquiries using the ChatGPT model.</p>
<h3 id="heading-installing-your-bot-to-slack">Installing Your Bot to Slack</h3>
<ol>
<li><p><strong>Add Your Bot to Slack</strong>: Navigate to your Slack workspace and access the "Apps" section. Search for your bot's name (e.g., "SlackAI") and add it to your workspace.</p>
</li>
<li><p><strong>Authorize Bot Permissions</strong>: Ensure you authorize the necessary permissions for your bot, including sending messages, interacting with users, and reading messages.</p>
</li>
<li><p><strong>Place Your Bot in a Channel</strong>: Choose a Slack channel where you'd like your bot to operate. Invite your bot to join the channel by mentioning it (e.g., <code>@SlackAI</code>).</p>
</li>
</ol>
<h3 id="heading-general-use-with-hugging-face-model">General Use with Hugging Face Model</h3>
<p>In this context, you can utilize the Hugging Face model for general conversations and inquiries. Mention your bot in the channel and ask a question or initiate a conversation. For example:</p>
<pre><code class="lang-sql">@SlackAI How's the weather today?
</code></pre>
<p>Your bot, powered by the Hugging Face model, will respond with intelligent insights or answers.</p>
<h3 id="heading-code-related-inquiries-with-chatgpt-model">Code-Related Inquiries with ChatGPT Model</h3>
<p>When you have code-related questions or need assistance with technical matters, you can invoke the ChatGPT model. Mention your bot again in the channel and frame your inquiry:</p>
<pre><code class="lang-sql">@SlackAI Can you <span class="hljs-keyword">help</span> me <span class="hljs-keyword">with</span> this Python code snippet?
</code></pre>
<p>Your bot will be ready to assist with coding-related queries, making your team's technical discussions more efficient.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this comprehensive exploration of creating a powerful Slack bot with AI capabilities using Supabase, you've gained the knowledge and tools to elevate your team's communication and productivity. By seamlessly integrating AI models and efficient queue management into your Slack bot, you've unlocked a world of automation and intelligent responses.</p>
<p>As you continue to refine and expand your bot's functionalities, consider these top-performing articles for further insights and optimizations:</p>
<ol>
<li><p><a target="_blank" href="https://blog.mansueli.com/using-custom-claims-testing-rls-with-supabase">Using Custom Claims &amp; Testing RLS with Supabase</a>: Dive deep into custom claims and role-level security with Supabase to enhance your application's data security.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/supabase-user-self-deletion-empower-users-with-edge-functions">Supabase User Self-Deletion: Empower Users with Edge Functions</a>: Learn how to implement user self-deletion features using Supabase Edge Functions, empowering users with control over their accounts.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/creating-customized-i18n-ready-authentication-emails-using-supabase-edge-functions-postgresql-and-resend">Creating Customized i18n-Ready Authentication Emails using Supabase Edge Functions, PostgreSQL, and Resend</a>: Dive into internationalization and create personalized authentication emails with Supabase Edge Functions and PostgreSQL.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test">Testing Supabase Edge Functions with Deno Test</a>: Explore best practices for testing your Supabase Edge Functions using Deno, ensuring robust functionality.</p>
</li>
</ol>
<p>These articles complement your journey to master Supabase and Slack bot development, offering valuable insights and strategies for enhancing your applications. Experimentation and customization are key to tailoring your bot to meet your project's specific needs.</p>
<h2 id="heading-references">References</h2>
<p>For additional guidance and resources, consider exploring these valuable references:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase Official Documentation</a>: Access the official documentation for Supabase to explore the platform's capabilities and features.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Official Documentation</a>: Dive into the official documentation of PostgreSQL, the powerful database system at the core of Supabase.</p>
</li>
<li><p><a target="_blank" href="https://deno.land/manual">Deno Documentation</a>: Explore Deno documentation for insights into this secure runtime for JavaScript and TypeScript.</p>
</li>
</ul>
<p>Building a Slack bot infused with AI capabilities offers an exciting opportunity to automate tasks and deliver intelligent responses, ultimately streamlining your team's interactions and fostering a more engaging work environment. Enjoy the journey of experimenting with various AI models and expanding your bot's functionalities!</p>
]]></content:encoded></item><item><title><![CDATA[Easy Deployment and Rollback of PostgreSQL Functions with Supabase]]></title><description><![CDATA[In the realm of database management, version control, and deployment are crucial. Efficiently deploying and managing database functions is vital for maintaining the integrity of your data-driven applications. While database migrations, as detailed in...]]></description><link>https://blog.mansueli.com/streamlining-postgresql-function-management-with-supabase</link><guid isPermaLink="true">https://blog.mansueli.com/streamlining-postgresql-function-management-with-supabase</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Databases]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 03 Oct 2023 16:24:32 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1696350043975/975d48e5-c57b-4163-a59f-368f0e03ac5c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the realm of database management, version control, and deployment are crucial. Efficiently deploying and managing database functions is vital for maintaining the integrity of your data-driven applications. While database migrations, as detailed in <a target="_blank" href="https://supabase.com/docs/reference/cli/supabase-migration">Supabase's migration guide</a>, are ideal for long-term projects, there are scenarios, such as prototyping and rapid development, where you need more flexibility.</p>
<p>In this blog post, we'll explore an approach tailored for quick prototyping and agile development—how to easily deploy and rollback PostgreSQL functions using <a target="_blank" href="https://supabase.com">Supabase</a>. Supabase, a powerful open-source alternative to traditional database management systems, simplifies the process of deploying and managing functions in these scenarios.</p>
<p>If you're working on more complex workflows or long-term projects, we highly recommend referring to <a target="_blank" href="https://supabase.com/docs/reference/cli/supabase-migration">Supabase's migration guide</a> for optimal version control and deployment practices.</p>
<h2 id="heading-postgresql-and-supabase-in-modern-web-applications">PostgreSQL and Supabase in Modern Web Applications</h2>
<p>PostgreSQL, a robust open-source relational database management system (RDBMS), has gained popularity in web development due to its reliability, extensibility, and support for complex data types.</p>
<p>Supabase, an open-source platform, offers various tools and services for modern web applications. It leverages PostgreSQL as its core database engine and provides a user-friendly interface for managing data, authentication, and more.</p>
<p>We'll explore how Supabase complements PostgreSQL by simplifying function deployment and rollback. You can refer to the <a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Documentation</a> to learn more about PostgreSQL.</p>
<h2 id="heading-tracking-function-history-in-postgresql">Tracking Function History in PostgreSQL</h2>
<p>When managing a PostgreSQL database, it's essential to track changes made to functions over time. This historical record allows you to review, audit, and revert to previous versions if needed.</p>
<p>To facilitate this, we'll create an <code>archive.function_history</code> table that stores crucial information about each function, including its name, arguments, return type, source code, and language settings.</p>
<p>Here's the SQL code for creating this table:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">archive</span>;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> archive.function_history (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">BIGINT</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">BY</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span>,
  schema_name <span class="hljs-built_in">text</span>,
  function_name <span class="hljs-built_in">text</span>,
  args <span class="hljs-built_in">text</span>,
  return_type <span class="hljs-built_in">text</span>,
  source_code <span class="hljs-built_in">text</span>,
  lang_settings <span class="hljs-built_in">text</span>,
  updated_at timestampz <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">now</span>(),
  <span class="hljs-keyword">version</span> <span class="hljs-built_in">NUMERIC</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">1</span>,
  <span class="hljs-keyword">CONSTRAINT</span> function_history_pkey PRIMARY <span class="hljs-keyword">KEY</span> (<span class="hljs-keyword">id</span>)
);

<span class="hljs-comment">--Handling version numbers automatically:</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> calculate_version()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Calculate the version number for new rows</span>
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">COALESCE</span>(<span class="hljs-keyword">MAX</span>(<span class="hljs-keyword">version</span>), <span class="hljs-number">0</span>) + <span class="hljs-number">1</span>
    <span class="hljs-keyword">INTO</span> NEW.version
    <span class="hljs-keyword">FROM</span> archive.function_history
    <span class="hljs-keyword">WHERE</span> schema_name = NEW.schema_name
      <span class="hljs-keyword">AND</span> function_name = NEW.function_name
      <span class="hljs-keyword">AND</span> return_type = NEW.return_type
      <span class="hljs-keyword">AND</span> args = NEW.args;

  RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> before_insert_function_history
<span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ON</span> archive.function_history
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> calculate_version();
</code></pre>
<h2 id="heading-saving-function-history">Saving Function History</h2>
<h3 id="heading-the-archivesavefunctionhistory-function">The <code>archive.save_function_history</code> Function</h3>
<p>To automate recording function changes, we'll create a PostgreSQL function called <code>archive.save_function_history</code>. This function takes parameters such as the function name, arguments, return type, source code, schema name, and language settings.</p>
<p>Here's the SQL code for creating the <code>archive.save_function_history</code> function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> 
<span class="hljs-keyword">FUNCTION</span> archive.save_function_history(
  function_name <span class="hljs-built_in">text</span>,
  args <span class="hljs-built_in">text</span>,
  return_type <span class="hljs-built_in">text</span>,
  source_code <span class="hljs-built_in">text</span>,
  schema_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'public'</span>,
  lang_settings <span class="hljs-built_in">text</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'plpgsql'</span>
) <span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">void</span> 
<span class="hljs-keyword">SET</span> search_path = <span class="hljs-keyword">public</span>, <span class="hljs-keyword">archive</span>
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">AS</span>
$$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> archive.function_history (
        schema_name, 
        function_name, 
        args, 
        return_type, 
        source_code, 
        lang_settings)
  <span class="hljs-keyword">VALUES</span> (schema_name, function_name, args, return_type, source_code, lang_settings);
<span class="hljs-keyword">END</span>;
$$
LANGUAGE plpgsql;
<span class="hljs-comment">-- Protecting the function:</span>
<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> 
archive.save_function_history <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">public</span>;

<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> 
archive.save_function_history <span class="hljs-keyword">FROM</span> anon, <span class="hljs-keyword">authenticated</span>;
</code></pre>
<p>This function allows us to easily store a snapshot of a function each time it's modified.</p>
<h2 id="heading-deploying-functions-from-source">Deploying Functions from Source</h2>
<h3 id="heading-the-createfunctionfromsource-function">The <code>create_function_from_source</code> Function</h3>
<p>Managing functions often involves deploying them from source code. PostgreSQL requires specific syntax for function creation, and Supabase simplifies this with the <code>create_function_from_source</code> function.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> 
create_function_from_source(
  function_text <span class="hljs-built_in">text</span>,
  schema_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'public'</span>
) <span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">text</span> 
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  function_name <span class="hljs-built_in">text</span>;
  argument_types text;
  return_type text;
  function_source text;
  lang_settings text;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Execute the function text to create the function</span>
  <span class="hljs-keyword">EXECUTE</span> function_text;

  <span class="hljs-comment">-- Extract function name from function text</span>
  <span class="hljs-keyword">SELECT</span> (regexp_matches(function_text, <span class="hljs-string">'create (or replace )?function (public\.)?(\w+)'</span>, <span class="hljs-string">'i'</span>))[<span class="hljs-number">3</span>]
  <span class="hljs-keyword">INTO</span> function_name;

  <span class="hljs-comment">-- Get function details from the system catalog</span>
  <span class="hljs-keyword">SELECT</span> pg_get_function_result(p.oid), 
                pg_get_function_arguments(p.oid), p.prosrc, l.lanname
  <span class="hljs-keyword">INTO</span> return_type, argument_types, function_source, lang_settings
  <span class="hljs-keyword">FROM</span> pg_proc p
  <span class="hljs-keyword">JOIN</span> pg_namespace n <span class="hljs-keyword">ON</span> n.oid = p.pronamespace
  <span class="hljs-keyword">JOIN</span> pg_language l <span class="hljs-keyword">ON</span> l.oid = p.prolang
  <span class="hljs-keyword">WHERE</span> n.nspname = schema_name <span class="hljs-keyword">AND</span> p.proname = function_name;

  <span class="hljs-comment">-- Save function history</span>
  PERFORM archive.save_function_history(function_name, argument_types, return_type, function_text, schema_name, lang_settings);

  RETURN 'Function created successfully.';
EXCEPTION
  WHEN others THEN
    RAISE EXCEPTION 'Error creating function: %', sqlerrm;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
<span class="hljs-comment">-- Protecting the function:</span>
<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> 
create_function_from_source <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">public</span>;

<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> 
create_function_from_source <span class="hljs-keyword">FROM</span> anon, <span class="hljs-keyword">authenticated</span>;
</code></pre>
<p>This function takes the function's SQL source code and schema name as parameters, creating the function within the database. It's a powerful tool for dynamic function creation.</p>
<p>Here's an example of deploying a function using <code>create_function_from_source</code>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> create_function_from_source(
$$
<span class="hljs-comment">-- Note that you can just paste the function below:</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> public.convert_to_uuid(input_value <span class="hljs-built_in">text</span>)
 <span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">uuid</span>
<span class="hljs-keyword">AS</span> $<span class="hljs-keyword">function</span>$
<span class="hljs-keyword">DECLARE</span>
  hash_hex <span class="hljs-built_in">text</span>;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Return null if input_value is null or an empty string</span>
  <span class="hljs-keyword">IF</span> input_value <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">NULLIF</span>(input_value, <span class="hljs-string">''</span>) <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">THEN</span>
    <span class="hljs-keyword">RETURN</span> <span class="hljs-literal">NULL</span>;
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
  hash_hex := substring(encode(digest(input_value::bytea, 'sha512'), 'hex'), 1, 36);
  RETURN (left(hash_hex, 8) || '-' || right(hash_hex, 4) || '-4' || right(hash_hex, 3) || '-a' || right(hash_hex, 3) || '-' || right(hash_hex, 12))::uuid;
<span class="hljs-keyword">END</span>;
$function$
LANGUAGE plpgsql
IMMUTABLE
SECURITY DEFINER;
<span class="hljs-comment">-- End of the function above</span>
$$
);
</code></pre>
<h2 id="heading-rolling-back-functions">Rolling Back Functions</h2>
<p>Rolling back functions is as crucial as deploying them. Mistakes happen, and being able to revert to a previous version can save valuable time and prevent data corruption.</p>
<p>The <code>rollback_function</code> function comes to the rescue. It retrieves the most recent function version from the <code>archive.function_history</code> table and executes it. If no previous version exists, it gracefully handles the situation.</p>
<p>Here's the SQL code for creating and using the <code>rollback_function</code>:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> rollback_function(
  func_name <span class="hljs-built_in">text</span>,
  schema_n <span class="hljs-built_in">text</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'public'</span>
) <span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">text</span> 
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  function_text <span class="hljs-built_in">text</span>;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Get the most recent function version from the function_history table</span>
  <span class="hljs-keyword">SELECT</span> source_code
  <span class="hljs-keyword">INTO</span> function_text
  <span class="hljs-keyword">FROM</span> archive.function_history
  <span class="hljs-keyword">WHERE</span> function_name = func_name <span class="hljs-keyword">AND</span> schema_name = schema_n
  <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> updated_at <span class="hljs-keyword">DESC</span>
  <span class="hljs-keyword">LIMIT</span> <span class="hljs-number">1</span>;

  <span class="hljs-comment">-- If no previous version is found, raise an error</span>
  IF function_text IS NULL THEN
    RAISE EXCEPTION 'No previous version of function % found.', func_name;
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

  <span class="hljs-comment">-- Add 'or replace' to the function text if it's not already there (case-insensitive search and replace)</span>
  IF NOT function_text ~* 'or <span class="hljs-keyword">replace</span><span class="hljs-string">' THEN
    function_text := regexp_replace(function_text, '</span><span class="hljs-keyword">create</span> <span class="hljs-keyword">function</span><span class="hljs-string">', '</span><span class="hljs-keyword">create</span> <span class="hljs-keyword">or</span> <span class="hljs-keyword">replace</span> <span class="hljs-keyword">function</span><span class="hljs-string">', '</span>i<span class="hljs-string">');
  END IF;

  -- Drop current version:
  EXECUTE format('</span><span class="hljs-keyword">DROP</span> <span class="hljs-keyword">FUNCTION</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">EXISTS</span> %I.%I<span class="hljs-string">', schema_n, func_name);
  -- Execute the function text to create the function
  EXECUTE function_text;

  RETURN '</span><span class="hljs-keyword">Function</span> rolled back successfully.<span class="hljs-string">';
EXCEPTION
  WHEN others THEN
    RAISE EXCEPTION '</span><span class="hljs-keyword">Error</span> <span class="hljs-keyword">rolling</span> back <span class="hljs-keyword">function</span>: %<span class="hljs-string">', sqlerrm;
END;
$$ LANGUAGE plpgsql;

-- Protecting the function:
REVOKE EXECUTE ON FUNCTION rollback_function FROM public;
REVOKE EXECUTE ON FUNCTION rollback_function FROM anon, authenticated;

-- Example of rolling back a function
SELECT rollback_function('</span>convert_to_uuid<span class="hljs-string">');</span>
</code></pre>
<h3 id="heading-setting-up-existing-functions-as-the-first-version">Setting up existing functions as the first version</h3>
<p>If you are starting with an existing database but want to start versioning from now. You can use this function below to archive all in the public schema.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> archive.setup_function_history(schema_name <span class="hljs-built_in">text</span> <span class="hljs-keyword">default</span> <span class="hljs-string">'public'</span>)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VOID</span> <span class="hljs-keyword">AS</span>
$$
<span class="hljs-keyword">DECLARE</span>
  function_record <span class="hljs-built_in">record</span>;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Loop through existing functions in the specified schema</span>
  <span class="hljs-keyword">FOR</span> function_record <span class="hljs-keyword">IN</span> (
    <span class="hljs-keyword">SELECT</span>
      n.nspname <span class="hljs-keyword">AS</span> schema_name,
      p.proname <span class="hljs-keyword">AS</span> function_name,
      pg_catalog.pg_get_function_arguments(p.oid) <span class="hljs-keyword">AS</span> args,
      pg_catalog.pg_get_function_result(p.oid) <span class="hljs-keyword">AS</span> return_type,
      pg_catalog.pg_get_functiondef(p.oid) <span class="hljs-keyword">AS</span> source_code,
      l.lanname <span class="hljs-keyword">AS</span> lang_settings
    <span class="hljs-keyword">FROM</span> pg_catalog.pg_proc p
    <span class="hljs-keyword">LEFT</span> <span class="hljs-keyword">JOIN</span> pg_catalog.pg_namespace n <span class="hljs-keyword">ON</span> n.oid = p.pronamespace
    <span class="hljs-keyword">LEFT</span> <span class="hljs-keyword">JOIN</span> pg_catalog.pg_language l <span class="hljs-keyword">ON</span> l.oid = p.prolang
    <span class="hljs-keyword">WHERE</span> n.nspname = schema_name
  )
  <span class="hljs-keyword">LOOP</span>
    <span class="hljs-comment">-- Insert information about the function into the history table</span>
    PERFORM archive.save_function_history(
      function_record.function_name,
      function_record.args,
      function_record.return_type,
      function_record.source_code,
      function_record.schema_name,
      function_record.lang_settings
    );
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">LOOP</span>;
<span class="hljs-keyword">END</span>;
$$
LANGUAGE plpgsql;

<span class="hljs-keyword">SELECT</span> archive.setup_function_history();
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, efficient management of PostgreSQL functions is crucial for web application development. Supabase, with its integration with PostgreSQL and the tools we've explored, offers a streamlined approach to function deployment and rollback.</p>
<p>Key takeaways from this blog post include the importance of function history tracking, the creation of the <code>archive.function_history</code> table, the <code>archive.save_function_history</code> function for recording changes, and the convenience of <code>create_function_from_text</code> and</p>
<p><code>rollback_function</code> for deployment and rollback.</p>
<p>If you found this article valuable, you might also be interested in exploring related topics:</p>
<ul>
<li><p><a target="_blank" href="https://blog.mansueli.com/rate-limiting-supabase-requests-with-postgresql-and-pgheaderkit">Rate Limiting Supabase Requests with PostgreSQL and pg_headerkit</a>: Learn how to implement rate limiting for Supabase requests.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/automating-webhooks-supabase-postgresql-guide">Comprehensive Guide: Deploying and Debugging Custom Webhooks on Supabase &amp; PostgreSQL</a>: Explore techniques for deploying and debugging custom webhooks.</p>
</li>
</ul>
<p>We encourage you to explore Supabase and PostgreSQL further to unlock the full potential of efficient database management.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<p>For further information and exploration, here are some additional resources:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/supabase/supabase">Supabase GitHub Repository</a></p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/">PostgreSQL Official Site</a></p>
</li>
</ul>
<p>Feel free to delve deeper into these resources to enhance your understanding of these powerful tools for database management.</p>
]]></content:encoded></item><item><title><![CDATA[Configuring Office365 as the SMTP Provider in Supabase Auth: A Comprehensive Guide]]></title><description><![CDATA[Email communication is a crucial aspect of many web applications, including those built on the Supabase platform. Whether it's for account verification, password resets, or general communication within the application, reliable email delivery is esse...]]></description><link>https://blog.mansueli.com/configuring-office365-as-the-smtp-provider-in-supabase-auth-a-comprehensive-guide</link><guid isPermaLink="true">https://blog.mansueli.com/configuring-office365-as-the-smtp-provider-in-supabase-auth-a-comprehensive-guide</guid><category><![CDATA[office365]]></category><category><![CDATA[smtp]]></category><category><![CDATA[authentication]]></category><category><![CDATA[supabase]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 26 Sep 2023 13:51:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695685407908/e82ad531-bdf1-4cc2-b04f-ea3cbfc0bba4.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Email communication is a crucial aspect of many web applications, including those built on the Supabase platform. Whether it's for account verification, password resets, or general communication within the application, reliable email delivery is essential. In this guide, we will address a common issue: "Sending emails from Supabase Auth via Office365 is not working as expected." We will guide you through the process of configuring Office365 for SMTP authentication to ensure seamless email communication within your Supabase applications.</p>
<h2 id="heading-understanding-the-issue">Understanding the Issue</h2>
<p>SMTP (Simple Mail Transfer Protocol) serves as the backbone for sending emails from web applications. When integrating GoTrue as an authentication service in Supabase, the platform relies on SMTP to send important email notifications to users. However, some users have reported issues with Office365 not allowing these emails to go through.</p>
<p>GoTrue is designed to streamline user authentication and management within Supabase applications. To ensure its features work seamlessly, the underlying email delivery system must be properly configured.</p>
<h3 id="heading-troubleshooting">Troubleshooting</h3>
<p>If you've encountered the problem of emails not being sent when using Office365 as the SMTP server, you may have noticed the following symptoms:</p>
<ul>
<li><strong>Failed Email Delivery</strong>: Emails not reaching their intended recipients.</li>
<li><strong>Authentication Issues</strong>: Error messages indicating authentication problems.</li>
<li><strong>Spam or Non-Delivery</strong>: Emails being marked as spam or not being delivered at all.</li>
</ul>
<h3 id="heading-why-office365-may-block-smtp-authentication">Why Office365 May Block SMTP Authentication</h3>
<p>Office365, like many email services, has security measures in place to prevent unauthorized use of its SMTP servers. By default, it may block SMTP authentication for specific mailboxes to protect against misuse. Understanding these security measures is crucial when configuring Office365 for SMTP in GoTrue.</p>
<h3 id="heading-solution-configuring-office365-for-smtp-authentication">Solution - Configuring Office365 for SMTP Authentication</h3>
<p>Now that we've grasped the issue's context and symptoms, let's delve into the solution: configuring Office365 for SMTP authentication.</p>
<h3 id="heading-step-by-step-guide">Step-by-Step Guide</h3>
<p><strong>Accessing <a target="_blank" href="http://supabase.com/">Supabase.com</a></strong></p>
<p>To begin, visit Supabase.com to learn more about Supabase and its features.</p>
<p><strong>Accessing the Office365 Admin Center</strong></p>
<p>Next, you'll need to access the <a target="_blank" href="https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#enable-smtp-auth-for-specific-mailboxes">Office365 Admin Center</a>. Follow these steps:</p>
<ul>
<li><strong>Log in to your Office365 account</strong> as an administrator.</li>
<li>Navigate to the <strong>Admin Center</strong>.</li>
</ul>
<p><strong>Navigating to Mailbox Settings</strong></p>
<p>Once you're in the Admin Center, proceed to find mailbox settings:</p>
<ul>
<li>Locate and click on the <strong>"Users"</strong> or <strong>"Active users"</strong> option.</li>
<li>Select the user whose mailbox settings you want to configure.</li>
</ul>
<p><strong>Enabling SMTP Authentication</strong></p>
<p>SMTP Authentication must be enabled for the selected mailbox. Here's how:</p>
<ul>
<li>Scroll down to the <strong>"Email apps"</strong> section.</li>
<li>Click <strong>"Manage email apps."</strong></li>
<li>Find <strong>"SMTP AUTH (SMTP Authentication)"</strong> and ensure it's enabled.</li>
</ul>
<h3 id="heading-alternative-method-powershell">Alternative Method: PowerShell</h3>
<p>In some cases, you may prefer using PowerShell for this configuration, especially if you need to configure multiple mailboxes. Here's the PowerShell command to enable SMTP authentication. If you never used PowerShell to connect with your Office365 Exchange services, run the following commands:</p>
<pre><code class="lang-powershell"><span class="hljs-built_in">Set-ExecutionPolicy</span> RemoteSigned
<span class="hljs-variable">$Session</span> = <span class="hljs-built_in">New-PSSession</span> <span class="hljs-literal">-ConfigurationName</span> Microsoft.Exchange <span class="hljs-literal">-ConnectionUri</span> https://outlook.office365.com/powershell<span class="hljs-literal">-liveid</span>/ <span class="hljs-literal">-Credential</span> <span class="hljs-variable">$UserCredential</span> <span class="hljs-literal">-Authentication</span> Basic <span class="hljs-literal">-AllowRedirection</span>
<span class="hljs-built_in">Import-PSSession</span> <span class="hljs-variable">$Session</span>
</code></pre>
<p>We can set this mailbox to allow SMTP Authentication now:</p>
<pre><code class="lang-powershell"><span class="hljs-built_in">Set-CASMailbox</span> <span class="hljs-literal">-Identity</span> &lt;MailboxIdentity&gt; <span class="hljs-literal">-SmtpClientAuthenticationDisabled</span> <span class="hljs-variable">$false</span>
</code></pre>
<p>In the command, replace <code>&lt;MailboxIdentity&gt;</code> with the actual mailbox you want to configure (e.g., <code>support@contoso.com</code>).</p>
<p>Using PowerShell provides flexibility and scalability, especially in larger organizations.</p>
<h2 id="heading-testing-the-configuration">Testing the Configuration</h2>
<p>With Office365 configured for SMTP authentication, it's essential to test the setup to ensure everything is working as expected. Testing helps verify that email notifications from GoTrue will be reliably delivered to your users.</p>
<p>To perform this test, you can use the <code>supabase.auth.admin.inviteUserByEmail</code> method provided by Supabase. This method allows you to send an invitation email to a specified email address. Here's how you can use it in JavaScript:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
  .auth.admin.inviteUserByEmail(<span class="hljs-string">'email@example.com'</span>);
</code></pre>
<p>You can find detailed information about this method in the <a target="_blank" href="https://supabase.com/docs/reference/javascript/auth-admin-inviteuserbyemail">Supabase documentation</a>.</p>
<p>By using this method, you can invite a dummy user to your app. If the email is sent successfully, it indicates that your SMTP configuration with Office365 is working correctly. However, if you encounter any errors or issues during this test, you may need to revisit your SMTP configuration settings and consult the troubleshooting tips in the next section to resolve the problem.</p>
<p>Testing is a crucial step in ensuring the reliability of your email communication within Supabase applications. Make sure to perform this test as part of your configuration process and periodically to verify ongoing functionality.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, configuring Office365 for SMTP authentication is a critical step in ensuring the smooth operation of email notifications within your Supabase applications. We have explored the background context of SMTP, and the integration of GoTrue in Supabase, and addressed the issue of emails not being delivered successfully. By following the step-by-step guide and considering alternative methods like PowerShell, you can overcome these challenges and provide reliable email communication to your users.</p>
<p>Remember that proper configuration and regular testing are key to maintaining a robust email system. We encourage you to follow best practices for email configuration and security to ensure the integrity of your email communications.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<p>For further reading and reference, here are some additional resources:</p>
<ul>
<li><a target="_blank" href="https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission#enable-smtp-auth-for-specific-mailboxes">Office365 Documentation</a></li>
<li><a target="_blank" href="http://supabase.com/docs">Supabase Documentation</a></li>
</ul>
<p>Please note that configurations and settings may change over time, so it's a good practice to refer to the official documentation for the most up-to-date information.</p>
]]></content:encoded></item><item><title><![CDATA[Building User Authentication with Username and Password using Supabase]]></title><description><![CDATA[User authentication is essential for web application security and user management. Supabase is an open-source platform that makes it easy to build secure authentication systems. In this blog post, we will show you how to enhance user login options by...]]></description><link>https://blog.mansueli.com/building-user-authentication-with-username-and-password-using-supabase</link><guid isPermaLink="true">https://blog.mansueli.com/building-user-authentication-with-username-and-password-using-supabase</guid><category><![CDATA[authentication]]></category><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 19 Sep 2023 13:45:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695129002603/14cb9317-3fa5-45a8-81e5-7d108a27c7d5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><strong>User authentication is essential for web application security and user management.</strong> <a target="_blank" href="https://supabase.com"><strong>Supabase</strong></a> <strong>is an open-source platform that makes it easy to build secure authentication systems. In this blog post, we will show you how to enhance user login options by enabling users to log in with their username and password or their email and password. You can check this example also on the</strong> <a target="_blank" href="https://github.com/mansueli/supabase-username-login"><strong>GitHub repo</strong></a><strong>.</strong></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we embark on this tutorial, it's crucial to have a foundational understanding of web development and Node.js. Familiarity with Next.js, a popular React framework, will also be advantageous. Additionally, you need Node.js and npm (Node Package Manager) installed on your system.</p>
<h2 id="heading-setting-up-the-project">Setting up the Project</h2>
<p>To kickstart our journey, let's create a new Next.js project integrated with Supabase. Execute the following command to establish the initial project structure:</p>
<pre><code class="lang-bash">npx create-next-app -e with-supabase username-login
</code></pre>
<p>This command will generate a Next.js project preconfigured with Supabase support with default authentication and routes.</p>
<h2 id="heading-configuring-supabase">Configuring Supabase</h2>
<p>Before we start coding, let's set up Supabase for our project. You need a Supabase project to get the credentials you need. Make sure you have your Supabase URL and API keys ready. It's important to keep these credentials safe, such as by using environment variables. You can find more information on configuring Supabase in the <a target="_blank" href="https://supabase.com/docs">Supabase documentation</a>.</p>
<h2 id="heading-tweaking-the-sign-in-route">Tweaking the Sign-In Route</h2>
<p>Now, let's closely examine the code responsible for user sign-in. Navigate to the <code>username-login/app/auth/sign-in/route.ts</code> file in your project. We will replace the default code for user authentication as follows:</p>
<p>Replace:</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({
  email,
  password,
})
</code></pre>
<p>With:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.functions.invoke(<span class="hljs-string">'sign-in'</span>, {
  body: {
    email,
    password,
  }
});

<span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.setSession({
  access_token: data.access_token,
  refresh_token: data.refresh_token
});
</code></pre>
<p>This code leverages Supabase Edge Functions to handle user sign-in, enabling users to choose between username and password or email for login.</p>
<h2 id="heading-edge-function-for-sign-in">Edge Function for Sign-In</h2>
<p>Within the authentication workflow, this edge function plays a pivotal role in managing sign-in requests. This function offers users greater flexibility by enabling them to use their usernames as an alternative login option. We'll be using this <a target="_blank" href="https://github.com/mansueli/supabase-username-login/blob/main/supabase/functions/_shared/cors.ts">cors.ts</a> file for handling the CORS headers:</p>
<pre><code class="lang-sql">export const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
</code></pre>
<p>To set up the Edge Function for Sign-In, create a Deno script as follows:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/std@0.192.0/http/server.ts'</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2'</span>
<span class="hljs-keyword">import</span> { corsHeaders } <span class="hljs-keyword">from</span> <span class="hljs-string">'../_shared/cors.ts'</span>

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Function "sign-in" is up and running!`</span>)

<span class="hljs-keyword">const</span> options =  {
  auth: {
    flowType: <span class="hljs-string">'implicit'</span>,
    autoRefreshToken: <span class="hljs-literal">false</span>,
    persistSession: <span class="hljs-literal">false</span>,
    detectSessionInUrl: <span class="hljs-literal">false</span>
  }
};

serve(<span class="hljs-keyword">async</span> (req: Request) =&gt; {
  <span class="hljs-comment">// This is needed if you're planning to invoke your function from a browser.</span>
  <span class="hljs-keyword">if</span> (req.method === <span class="hljs-string">'OPTIONS'</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>, { headers: corsHeaders })
  }
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> supabaseAdmin = createClient(
      Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>) ?? <span class="hljs-string">''</span>,
      Deno.env.get(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>) ?? <span class="hljs-string">''</span>,
      options
    )
    <span class="hljs-keyword">const</span> { email, password } = <span class="hljs-keyword">await</span> req.json()
    <span class="hljs-comment">// Create a Supabase client with the Auth context of the logged-in user.</span>
    <span class="hljs-keyword">const</span> supabaseClient = createClient(
      <span class="hljs-comment">// Supabase API URL - env var exported by default.</span>
      Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>) ?? <span class="hljs-string">''</span>,
      <span class="hljs-comment">// Supabase API ANON KEY - env var exported by default.</span>
      Deno.env.get(<span class="hljs-string">'SUPABASE_ANON_KEY'</span>) ?? <span class="hljs-string">''</span>,
      options
    )
    <span class="hljs-keyword">const</span> { data: profileData, error: userError } = <span class="hljs-keyword">await</span> supabaseAdmin
      .from(<span class="hljs-string">'profiles'</span>)
      .select(<span class="hljs-string">'email'</span>)
      .or(<span class="hljs-string">`username.eq.<span class="hljs-subst">${email}</span>,email.eq.<span class="hljs-subst">${email}</span>`</span>)
      .limit(<span class="hljs-number">1</span>)
      .maybeSingle();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"profileData:"</span> + <span class="hljs-built_in">JSON</span>.stringify(profileData, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>))
    <span class="hljs-keyword">if</span> (userError) <span class="hljs-keyword">throw</span> userError

    <span class="hljs-keyword">const</span> { data: { session }, error } = <span class="hljs-keyword">await</span> supabaseClient.auth.signInWithPassword({
      email: profileData.email,
      password: password,
    })
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"data:"</span> + <span class="hljs-built_in">JSON</span>.stringify(session, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>))
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(session), {
      headers: { ...corsHeaders, <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
      status: <span class="hljs-number">200</span>,
    });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: error }), {
      headers: { ...corsHeaders, <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
      status: <span class="hljs-number">400</span>,
    })
  }
})
</code></pre>
<p>This Deno script handles sign-in requests and allows users to choose between usernames and email for login. It leverages Supabase Edge Functions, enhancing the flexibility of your authentication system.</p>
<p>To learn more about testing Supabase Edge Functions, check our dedicated blog post on <a target="_blank" href="https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test">Testing Supabase Edge Functions with Deno Test</a>.</p>
<p>Here's an example structure for the <code>public.profiles</code> table that you can use in your project:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> public.profiles (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">uuid</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  updated_at <span class="hljs-built_in">timestamp</span> <span class="hljs-keyword">with</span> <span class="hljs-built_in">time</span> zone <span class="hljs-literal">NULL</span>,
  username <span class="hljs-built_in">text</span> <span class="hljs-literal">NULL</span>,
  full_name <span class="hljs-built_in">text</span> <span class="hljs-literal">NULL</span>,
  avatar_url <span class="hljs-built_in">text</span> <span class="hljs-literal">NULL</span>,
  website <span class="hljs-built_in">text</span> <span class="hljs-literal">NULL</span>,
  email <span class="hljs-built_in">text</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">CONSTRAINT</span> profiles_pkey PRIMARY <span class="hljs-keyword">KEY</span> (<span class="hljs-keyword">id</span>),
  <span class="hljs-keyword">CONSTRAINT</span> profiles_username_key <span class="hljs-keyword">UNIQUE</span> (username),
  <span class="hljs-keyword">CONSTRAINT</span> profiles_id_fkey <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (<span class="hljs-keyword">id</span>) <span class="hljs-keyword">REFERENCES</span> auth.users (<span class="hljs-keyword">id</span>) <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  <span class="hljs-keyword">CONSTRAINT</span> username_length <span class="hljs-keyword">CHECK</span> (<span class="hljs-keyword">char_length</span>(username) &gt;= <span class="hljs-number">3</span>)
);
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> public.profiles <span class="hljs-keyword">ENABLE</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">LEVEL</span> <span class="hljs-keyword">SECURITY</span>;
</code></pre>
<p>You can read more about creating such a table in <a target="_blank" href="https://supabase.com/docs/guides/auth/managing-user-data#creating-user-tables">Managing User Data</a>.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this extensive exploration of user authentication with Supabase, we've equipped you with the knowledge and tools to build secure and flexible login systems for your web applications. By enabling users to choose between username and password or email and password login methods, you've enhanced the user experience.</p>
<p>As you continue to develop and fine-tune your authentication systems, consider the following articles for further insights and optimizations:</p>
<ul>
<li><p><a target="_blank" href="https://blog.mansueli.com/automating-webhooks-supabase-postgresql-guide">Comprehensive Guide: Deploying and Debugging Custom Webhooks on Supabase &amp; PostgreSQL</a>: Learn how to automate webhooks for seamless integration into your applications.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/creating-customized-i18n-ready-authentication-emails-using-supabase-edge-functions-postgresql-and-resend">Creating Customized i18n-Ready Authentication Emails using Supabase Edge Functions, PostgreSQL, and Resend</a>: Dive into the world of internationalization and create personalized authentication emails.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/how-to-boost-supabase-reliability-a-guide-to-using-postgres-foreign-data-wrappers">How to Boost Supabase Reliability: A Guide to Using Postgres Foreign Data Wrappers</a>: Enhance the reliability of your Supabase applications with foreign data wrappers.</p>
</li>
</ul>
<p>These articles will complement your journey to master Supabase authentication and take your web development skills to the next level. Experimentation and customization are key to tailoring authentication systems to your specific project needs.</p>
<h2 id="heading-references">References</h2>
<p>For additional guidance and resources, consider exploring these valuable references:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase Official Documentation</a>: Explore Supabase's official documentation for comprehensive insights into this robust backend platform.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Official Documentation</a>: Dive into the official documentation of PostgreSQL, the powerful database system at the core of Supabase.</p>
</li>
<li><p><a target="_blank" href="https://nextjs.org/docs">Next.js Documentation</a>: Access Next.js documentation for further information on this popular React framework.</p>
</li>
<li><p><a target="_blank" href="https://deno.land/manual">Deno Documentation</a>: Explore Deno documentation for insights into this secure runtime for JavaScript and TypeScript.</p>
</li>
</ul>
<p>With these references and your newfound knowledge, you're well-prepared to create exceptional web applications powered by Supabase. Continue your journey in web development, and don't hesitate to explore additional features and customization options offered by Supabase and PostgreSQL.</p>
]]></content:encoded></item><item><title><![CDATA[Migrating from MongoDB to Supabase with PostgreSQL]]></title><description><![CDATA[In the realm of database management, making informed decisions can significantly impact your application's performance and scalability. This comprehensive guide will offer you one possible way of migrating from MongoDB to Supabase with PostgreSQL. Th...]]></description><link>https://blog.mansueli.com/migrating-from-mongodb-to-supabase-with-postgresql</link><guid isPermaLink="true">https://blog.mansueli.com/migrating-from-mongodb-to-supabase-with-postgresql</guid><category><![CDATA[MongoDB]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[migration]]></category><category><![CDATA[data migration]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 12 Sep 2023 20:42:25 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694550893974/f679a36c-45a4-455f-bb57-60c26aea555b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the realm of database management, making informed decisions can significantly impact your application's performance and scalability. This comprehensive guide will offer you one possible way of migrating from MongoDB to <a target="_blank" href="http://supabase.com/">Supabase</a> with PostgreSQL. There's an ongoing effort to improve this migration to make it seamless.</p>
<h3 id="heading-why-migrate-from-mongodb-to-supabase-with-postgresql">Why Migrate from MongoDB to Supabase with PostgreSQL?</h3>
<p>MongoDB has been a go-to choice for its flexibility, but as your project matures, scalability and complex querying challenges may arise. Supabase, built on PostgreSQL, combines the best of both worlds—a flexible NoSQL-style database with the reliability and performance of PostgreSQL. This migration opens up new possibilities for your application, making it a vital step for your project's growth.</p>
<h2 id="heading-understanding-the-migration-process">Understanding the Migration Process</h2>
<h3 id="heading-why-choose-supabase-and-postgresql-for-your-migration">Why Choose Supabase and PostgreSQL for Your Migration?</h3>
<p>To kick off your migration journey, let's delve into why Supabase and PostgreSQL are the great choices:</p>
<ul>
<li><p><strong>Scalability</strong>: Supabase, powered by PostgreSQL, offers superb scalability, making it a fit for projects of all sizes.</p>
</li>
<li><p><strong>Performance</strong>: PostgreSQL is renowned for its speed and efficiency, ensuring smooth application operation even as your data grows.</p>
</li>
<li><p><strong>Ease of Use</strong>: Supabase simplifies database management with an intuitive interface, catering to developers of all skill levels.</p>
</li>
</ul>
<h3 id="heading-planning-the-migration-process">Planning the Migration Process</h3>
<p>Before diving into the technical aspects, meticulous planning is paramount for a seamless migration. Considerations like data mapping, schema design, and data transformation require careful attention. As discussed in our previous article on <a target="_blank" href="https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql">"Exploring Data Relationships with Supabase and PostgreSQL"</a>, understanding your data's structure is a crucial initial step.</p>
<h2 id="heading-running-the-migration">Running the Migration</h2>
<p>You can run the migration process using this Colab notebook I've prepared for your convenience. It provides step-by-step instructions to ensure a smooth transition from MongoDB to Supabase with PostgreSQL. Below, we'll go through the steps to set it up yourself:</p>
<h2 id="heading-preparing-your-environment">Preparing Your Environment</h2>
<h3 id="heading-installing-the-required-python-libraries">Installing the Required Python Libraries</h3>
<p>Let's begin by installing the essential Python libraries for your migration journey:</p>
<pre><code class="lang-python">pip install mongo
pip install psycopg2
</code></pre>
<h2 id="heading-setting-up-connection-uris">Setting Up Connection URIs</h2>
<p>To begin the migration process, it's essential to configure the connection URIs for both MongoDB and Supabase. Make sure you have your credentials ready and set the environment variables as shown below:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Source DB variables:</span>
%env supabase_uri=postgresql://postgres:password@db.xxasaxx.supabase.co:<span class="hljs-number">5432</span>/
%env mongo_uri=mongodb+srv://nacho:password@cluster001.jjj.mongodb.net/?retryWrites=true&amp;w=majority
%env mongo_db=sample_mflix
</code></pre>
<h2 id="heading-running-the-migration-manually">Running the Migration Manually</h2>
<h3 id="heading-mapping-data-types">Mapping Data Types</h3>
<p>Mapping data types from MongoDB to PostgreSQL is a crucial step in the migration process. The provided script handles these conversions intelligently based on the Python types encountered in your MongoDB data. For example, it recognizes <code>ObjectId</code> and correctly maps it to the equivalent PostgreSQL data type. This ensures that your data retains its integrity throughout the migration process.</p>
<p>Here's how data types are mapped in the script:</p>
<pre><code class="lang-python"><span class="hljs-comment"># Mapping MongoDB types to PostgreSQL types</span>
SQL_DATA_TYPE = {
    <span class="hljs-string">"string"</span>: <span class="hljs-string">"TEXT"</span>,
    <span class="hljs-string">"ObjectId"</span>: <span class="hljs-string">"TEXT"</span>,
    <span class="hljs-string">"datetime"</span>: <span class="hljs-string">"TIMESTAMP WITH TIME ZONE"</span>,
    <span class="hljs-string">"int"</span>: <span class="hljs-string">"INT"</span>,
    <span class="hljs-string">"list"</span>: <span class="hljs-string">"JSONB"</span>,
    <span class="hljs-string">"dict"</span>: <span class="hljs-string">"JSONB"</span>,
    <span class="hljs-string">"bool"</span>: <span class="hljs-string">"Boolean"</span>,
    <span class="hljs-string">"float"</span>: <span class="hljs-string">"NUMERIC"</span>,
    <span class="hljs-string">"default"</span>: <span class="hljs-string">"TEXT"</span>,
}
</code></pre>
<h3 id="heading-creating-postgresql-tables">Creating PostgreSQL Tables</h3>
<p>Creating the necessary tables in PostgreSQL is seamlessly handled by the provided script. If a table doesn't already exist for a MongoDB collection, the script creates one. Additionally, it checks for existing tables to avoid duplicates. This ensures that your data is organized efficiently in the PostgreSQL database, making it ready for further use.</p>
<p>In the script, you have the flexibility to specify the target MongoDB database using the <code>mongo_db_manual</code> variable. If left empty, the script will run for all databases in MongoDB.</p>
<p>For databases with more resources, consider including <code>collection.find(no_cursor_timeout=True)</code> in the code to prevent cursor timeouts during the migration.</p>
<h2 id="heading-migration-code">Migration code</h2>
<p>Here's the migration code that establishes connections, maps data types, and creates PostgreSQL tables:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> bson.decimal128 <span class="hljs-keyword">import</span> Decimal128
<span class="hljs-keyword">import</span> pymongo
<span class="hljs-keyword">import</span> psycopg2
<span class="hljs-keyword">from</span> psycopg2.extensions <span class="hljs-keyword">import</span> AsIs
<span class="hljs-keyword">import</span> json
<span class="hljs-keyword">from</span> datetime <span class="hljs-keyword">import</span> datetime
<span class="hljs-keyword">from</span> psycopg2 <span class="hljs-keyword">import</span> sql, extensions, connect, Error
<span class="hljs-keyword">from</span> bson <span class="hljs-keyword">import</span> ObjectId
<span class="hljs-keyword">import</span> os

mongo_url = os.environ[<span class="hljs-string">'mongo_uri'</span>]
supabase_url = os.environ[<span class="hljs-string">'supabase_uri'</span>]

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomEncoder</span>(<span class="hljs-params">json.JSONEncoder</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">default</span>(<span class="hljs-params">self, obj</span>):</span>
        <span class="hljs-keyword">if</span> isinstance(obj, ObjectId):
          <span class="hljs-keyword">return</span> str(obj)
        <span class="hljs-keyword">if</span> isinstance(obj, datetime):
          <span class="hljs-keyword">return</span> obj.isoformat()
        <span class="hljs-keyword">if</span> isinstance(obj, Decimal128):
          <span class="hljs-keyword">return</span> str(obj)
        <span class="hljs-keyword">if</span> isinstance(obj, complex):
            <span class="hljs-keyword">return</span> [obj.real, obj.imag]
        <span class="hljs-keyword">return</span> json.JSONEncoder.default(self, obj)


psycopg2.extensions.register_adapter(Decimal128, <span class="hljs-keyword">lambda</span> val: AsIs(str(val.to_decimal())))

<span class="hljs-comment"># Connect to MongoDB</span>
mongo_client = pymongo.MongoClient(mongo_url)
<span class="hljs-comment"># Connect to PostgreSQL</span>
pg_conn = connect(supabase_url)
pg_conn.set_isolation_level(extensions.ISOLATION_LEVEL_AUTOCOMMIT)
pg_cur = pg_conn.cursor()

<span class="hljs-comment"># Mapping MongoDB types to PostgreSQL types</span>
SQL_DATA_TYPE = {
  <span class="hljs-string">"str"</span>: <span class="hljs-string">"TEXT"</span>,
  <span class="hljs-string">"ObjectId"</span>: <span class="hljs-string">"TEXT"</span>,
  <span class="hljs-string">"datetime.datetime"</span>: <span class="hljs-string">"TIMESTAMP WITH TIME ZONE"</span>,
  <span class="hljs-string">"datetime"</span>: <span class="hljs-string">"TIMESTAMP WITH TIME ZONE"</span>,
  <span class="hljs-string">"int"</span>: <span class="hljs-string">"INT"</span>,
  <span class="hljs-string">"list"</span>: <span class="hljs-string">"JSONB"</span>,
  <span class="hljs-string">"dict"</span>: <span class="hljs-string">"JSONB"</span>,
  <span class="hljs-string">"bool"</span>: <span class="hljs-string">"Boolean"</span>,
  <span class="hljs-string">"float"</span>: <span class="hljs-string">"NUMERIC"</span>,
  <span class="hljs-string">"default"</span>: <span class="hljs-string">"TEXT"</span>,
  <span class="hljs-string">"NoneType"</span>:<span class="hljs-string">"TEXT"</span>,
  <span class="hljs-string">"Decimal128"</span>:<span class="hljs-string">"NUMERIC"</span>,
}

<span class="hljs-comment"># Store the type of each field</span>
field_types = {}

<span class="hljs-comment"># Get the list of database names from MongoDB</span>
mongo_db_manual = os.environ[<span class="hljs-string">'mongo_db'</span>]
mongo_db_names = []
<span class="hljs-keyword">if</span>(len(mongo_db_manual)&gt;<span class="hljs-number">0</span>):
  mongo_db_names.append(mongo_db_manual)
<span class="hljs-keyword">else</span>:
  mongo_db_names = mongo_client.list_database_names()

<span class="hljs-comment"># Iterate over all MongoDB databases</span>
<span class="hljs-keyword">for</span> db_name <span class="hljs-keyword">in</span> mongo_db_names:
    print(<span class="hljs-string">"Starting to migrate :"</span>+ str(db_name))
    mongo_db = mongo_client[db_name]

    <span class="hljs-comment"># Iterate over all collections in the current database</span>
    <span class="hljs-keyword">for</span> collection_name <span class="hljs-keyword">in</span> mongo_db.list_collection_names():
        <span class="hljs-comment"># Skip system collections</span>
        <span class="hljs-keyword">if</span> collection_name.startswith(<span class="hljs-string">"system."</span>):
            <span class="hljs-keyword">continue</span>

        collection = mongo_db[collection_name]
        <span class="hljs-comment"># Create table in PostgreSQL if it doesn't exist</span>
        pg_cur.execute(sql.SQL(<span class="hljs-string">"CREATE TABLE IF NOT EXISTS {} ()"</span>).format(
            sql.Identifier(collection_name)))

        <span class="hljs-comment"># Iterate over all documents in the collection</span>
        cursor = collection.find()
        <span class="hljs-keyword">for</span> document <span class="hljs-keyword">in</span> cursor:
            <span class="hljs-comment"># For each document, build a list of fields and a list of values</span>
            fields = []
            values = []
            <span class="hljs-keyword">for</span> field, value <span class="hljs-keyword">in</span> document.items():
                <span class="hljs-comment"># Determine PostgreSQL type based on Python type</span>
                <span class="hljs-keyword">if</span> isinstance(value, ObjectId):
                    pg_type = SQL_DATA_TYPE[<span class="hljs-string">"ObjectId"</span>]
                    value = str(value)
                <span class="hljs-keyword">else</span>:
                    pg_type = SQL_DATA_TYPE.get(type(value).__name__, SQL_DATA_TYPE[<span class="hljs-string">"default"</span>])

                <span class="hljs-comment"># Add type suffix to field name if a new type is encountered</span>
                field_with_type = field
                <span class="hljs-keyword">if</span> field <span class="hljs-keyword">in</span> field_types:
                    <span class="hljs-keyword">if</span> type(value).__name__ <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> field_types[field]:
                        field_types[field].add(type(value).__name__)
                        field_with_type = <span class="hljs-string">f"<span class="hljs-subst">{field}</span>_<span class="hljs-subst">{type(value).__name__}</span>"</span>
                <span class="hljs-keyword">else</span>:
                    field_types[field] = {type(value).__name__}

                <span class="hljs-comment"># Add column in PostgreSQL if it doesn't exist</span>
                <span class="hljs-keyword">try</span>:
                    pg_cur.execute(sql.SQL(<span class="hljs-string">"ALTER TABLE {} ADD COLUMN {} {}"</span>).format(
                        sql.Identifier(collection_name),
                        sql.Identifier(field_with_type),
                        sql.SQL(pg_type)))
                <span class="hljs-keyword">except</span> Error:
                    <span class="hljs-keyword">pass</span>  <span class="hljs-comment"># Column already exists, no action needed</span>

                <span class="hljs-comment"># Add field and value to the lists</span>
                fields.append(sql.Identifier(field_with_type))
                <span class="hljs-keyword">if</span> isinstance(value, list) <span class="hljs-keyword">or</span> isinstance(value, dict):
                    value = json.dumps(value, cls=CustomEncoder)
                values.append(value)

            <span class="hljs-comment"># Insert data into PostgreSQL</span>
            pg_cur.execute(sql.SQL(<span class="hljs-string">"INSERT INTO {} ({}) VALUES ({})"</span>).format(
                sql.Identifier(collection_name),
                sql.SQL(<span class="hljs-string">', '</span>).join(fields),
                sql.SQL(<span class="hljs-string">', '</span>).join(sql.Placeholder() * len(values))),
                values)

pg_cur.close()
pg_conn.close()
</code></pre>
<p>This script simplifies the migration process by automating many of the essential tasks.</p>
<h2 id="heading-data-transformation-and-beyond">Data Transformation and Beyond</h2>
<h3 id="heading-making-the-transition">Making the Transition</h3>
<p>With your connection set up and tables created, it's time to dive into the heart of the migration process - transforming MongoDB documents into PostgreSQL rows. This step is pivotal for a successful migration and ensuring that your data remains intact.</p>
<p><strong>The Data Transformation Journey</strong></p>
<p>The journey of transforming your data from MongoDB to PostgreSQL is where the magic happens. MongoDB and PostgreSQL have distinct data models, which means a meticulous transformation process is crucial. We won't just guide you through this journey; we'll take you on a hands-on tour with detailed code samples and explanations.</p>
<p><strong>Code in Action</strong></p>
<p>Our approach is all about clarity and understanding. We provide code samples that vividly showcase the transformation of MongoDB data into a PostgreSQL-compatible format. Each sample is accompanied by a comprehensive explanation, ensuring that you not only get the job done but also understand the underlying nuances and intricacies.</p>
<p><strong>Exploring Alternatives</strong></p>
<p>While our primary focus has been on migrating from MongoDB to PostgreSQL, it's always a good practice to explore alternative solutions. One compelling alternative is <a target="_blank" href="https://www.ferretdb.io/">FerretDB</a>, a MongoDB-like database using PostgreSQL that can be hosted within the Supabase ecosystem. Depending on your specific requirements and preferences, this might be an excellent choice to consider.</p>
<h3 id="heading-handling-challenges">Handling Challenges</h3>
<p>Migration projects often encounter unexpected challenges. It's essential to acknowledge that no migration process is entirely free of hurdles. While we've covered a broad spectrum of challenges and provided solutions, unique scenarios may require specialized attention. In such cases, it's advisable to seek professional guidance to ensure a seamless migration experience.</p>
<h2 id="heading-post-migration-considerations">Post-Migration Considerations</h2>
<p>With your data successfully migrated to PostgreSQL through Supabase, let's shift our focus to what comes next - ensuring your database performs optimally and reliably.</p>
<h3 id="heading-data-validation-and-optimization">Data Validation and Optimization</h3>
<p>After completing the migration, your first priority should be data validation and testing. It's not just about getting your data into the new system; it's about making sure it made the transition accurately and retains its integrity. Learn about best practices and available tools for this crucial step. We'll guide you through the process, leaving no room for uncertainty.</p>
<p><strong>Unleashing PostgreSQL's Power</strong></p>
<p>To fully harness the capabilities of PostgreSQL, it's essential to optimize your database's performance. This section is a treasure trove of insights into key areas like indexing, query optimization, and other relevant topics. Discover how to fine-tune your database for optimal speed and efficiency, ensuring that your application runs smoothly.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In conclusion, migrating from MongoDB to Supabase with PostgreSQL can be a transformative step for your application. We've covered crucial stages, from understanding the advantages of this transition to handling post-migration considerations. Please note that this is an initial effort migrating from live connections and is not performant at this time. I am also working on an alternative migration guide from <code>mongodump</code> to Postgres in this <a target="_blank" href="https://github.com/mansueli/mongo-2-postgres/t">GitHub repo</a>. Contributions are very welcome.</p>
<h3 id="heading-additional-resources">Additional Resources</h3>
<p>To further assist you in your migration journey, here are some additional resources:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase Documentation</a>: Explore Supabase's official documentation for in-depth information and guidance.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Official Documentation</a>: Dive into the official PostgreSQL documentation to expand your knowledge of this powerful database system.</p>
</li>
<li><p><a target="_blank" href="https://github.com/mansueli/Supa-Migrate/blob/main/mongo2supabase.ipynb">GitHub Repository with Migration Code Samples</a>: Access our GitHub repository, where you can find detailed code samples and resources related to the migration process.</p>
</li>
</ul>
<p>Feel free to explore these resources as you embark on your journey to a more efficient and robust database solution. Happy migration!</p>
]]></content:encoded></item><item><title><![CDATA[Rate Limiting Supabase Requests with PostgreSQL and pg_headerkit]]></title><description><![CDATA[Introduction
Rate limiting is a critical aspect of web applications that ensures fair usage of resources and prevents abuse. In this blog post, we'll explore how to implement rate limiting for Supabase requests using PostgreSQL and the pg_headerkit e...]]></description><link>https://blog.mansueli.com/rate-limiting-supabase-requests-with-postgresql-and-pgheaderkit</link><guid isPermaLink="true">https://blog.mansueli.com/rate-limiting-supabase-requests-with-postgresql-and-pgheaderkit</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[ratelimit]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Databases]]></category><category><![CDATA[APIs]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 05 Sep 2023 11:57:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693690195937/89768eca-c96e-4095-b410-45a115a6b194.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2 id="heading-introduction">Introduction</h2>
<p>Rate limiting is a critical aspect of web applications that ensures fair usage of resources and prevents abuse. In this blog post, we'll explore how to implement rate limiting for <a target="_blank" href="https://supabase.com/">Supabase</a> requests using PostgreSQL and the pg_headerkit extension. This article is part of a series on optimizing Supabase performance, and it builds upon our previous guide on <a target="_blank" href="https://blog.mansueli.com/how-to-boost-supabase-reliability-a-guide-to-using-postgres-foreign-data-wrappers">Boosting Supabase Reliability</a>.</p>
<p>Supabase, a powerful backend platform built on top of PostgreSQL, relies on PostgreSQL as its underlying database. By leveraging PostgreSQL and pg_headerkit, we can efficiently control the rate at which requests are made to Supabase, ensuring optimal performance and resource allocation.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive into the implementation, make sure you have the following prerequisites:</p>
<ol>
<li><p><strong>Supabase Project</strong>: Ensure you have an existing Supabase project with the necessary API endpoints set up.</p>
</li>
<li><p><strong>PostgreSQL Database</strong>: Use PostgreSQL as your backend database for Supabase. If you haven't set up PostgreSQL with Supabase yet, follow the official documentation to get started.</p>
</li>
<li><p><strong>pg_headerkit</strong>: You will need to install the pg_headerkit library. Find installation instructions and more information on this library at <a target="_blank" href="https://database.dev/burggraf/pg_headerkit">https://database.dev/burggraf/pg_headerkit</a>.</p>
</li>
</ol>
<h2 id="heading-setting-up-the-environment">Setting up the Environment</h2>
<p>Before we dive into rate limiting, let's ensure we have the necessary prerequisites in place:</p>
<ol>
<li><p><strong>Supabase Account</strong>: Make sure you have a Supabase account set up.</p>
</li>
<li><p><strong>Database.dev</strong>: Install dbdev using <a target="_blank" href="https://database.dev/installer">https://database.dev/installer</a>.</p>
</li>
</ol>
<p>Next, let's install and set up pg_headerkit:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> dbdev.install(<span class="hljs-string">'burggraf-pg_headerkit'</span>);
<span class="hljs-keyword">CREATE</span> EXTENSION <span class="hljs-string">"burggraf-pg_headerkit"</span> <span class="hljs-keyword">VERSION</span> <span class="hljs-string">'1.0.0'</span>;
</code></pre>
<h2 id="heading-creating-the-rate-limiting-infrastructure">Creating the Rate Limiting Infrastructure</h2>
<p>In this section, we'll dive into the process of creating the essential infrastructure for rate limiting within your Supabase-powered application. Rate limiting is a crucial mechanism that allows you to control the number of requests made to your Supabase endpoints, ensuring fair usage of resources and maintaining system stability.</p>
<h3 id="heading-the-requestlog-table">The <code>request_log</code> Table</h3>
<p>We begin with the creation of the <code>request_log</code> table. This table serves as a main component for tracking and monitoring incoming requests. Here's how we set it up:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> UNLOGGED <span class="hljs-keyword">TABLE</span> request_log (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">BIGINT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> primary <span class="hljs-keyword">key</span>,
  ip inet <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-built_in">timestamp</span> timestamptz <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>()
);

<span class="hljs-keyword">CREATE</span> UNLOGGED <span class="hljs-keyword">SEQUENCE</span> request_log_id_seq    
    <span class="hljs-keyword">START</span> <span class="hljs-keyword">WITH</span> <span class="hljs-number">1</span>
    <span class="hljs-keyword">INCREMENT</span> <span class="hljs-keyword">BY</span> <span class="hljs-number">1</span>
    <span class="hljs-keyword">NO</span> <span class="hljs-keyword">MINVALUE</span>
    <span class="hljs-keyword">NO</span> MAXVALUE
    <span class="hljs-keyword">CACHE</span> <span class="hljs-number">1</span>;

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> request_log <span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">COLUMN</span> <span class="hljs-keyword">id</span> <span class="hljs-keyword">SET</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">nextval</span>(<span class="hljs-string">'request_log_id_seq'</span>);
</code></pre>
<p>The <code>request_log</code> table has three essential columns:</p>
<ul>
<li><p><code>id</code>: A unique identifier for each log entry, automatically generated.</p>
</li>
<li><p><code>ip</code>: This column captures the client's IP address, helping us identify the source of each request.</p>
</li>
<li><p><code>timestamp</code>: It records the exact time each request was made, ensuring accurate tracking.</p>
</li>
</ul>
<h3 id="heading-the-registerrequest-function">The <code>register_request</code> Function</h3>
<p>With the <code>request_log</code> table in place, we proceed to create the <code>register_request</code> function. This function plays a pivotal role in the rate-limiting process by logging every incoming request and associating it with the client's IP address. Here's how it's defined:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> register_request(ip_in <span class="hljs-built_in">TEXT</span>) 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VOID</span> 
<span class="hljs-keyword">LANGUAGE</span> plpgsql <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> request_log (ip) 
  <span class="hljs-keyword">VALUES</span> (inet(ip_in));
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<p>The <code>register_request</code> function takes the client's IP address as input and inserts a corresponding entry into the <code>request_log</code> table. This action ensures that we have a comprehensive record of all incoming requests, which is essential for rate limiting and analytics.</p>
<p>With the infrastructure for tracking requests established, we're now ready to move forward with the rate-limiting implementation. In the following sections, we'll explore how to set rate limits and enforce them effectively.</p>
<h2 id="heading-cleaning-old-requests">Cleaning Old Requests</h2>
<p>To maintain the efficiency of our system, it's crucial to regularly clean up old request logs. The <code>clean_old_requests</code> function takes care of this task:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> clean_old_requests() 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VOID</span> 
<span class="hljs-keyword">LANGUAGE</span> plpgsql <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Delete request logs older than 12 hours</span>
  <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> request_log 
  <span class="hljs-keyword">WHERE</span> <span class="hljs-built_in">timestamp</span> &lt; <span class="hljs-keyword">NOW</span>() - <span class="hljs-built_in">INTERVAL</span> <span class="hljs-string">'12 hours'</span>;
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<p>This function ensures that our database remains clutter-free and retains only the most relevant request data.</p>
<h2 id="heading-implementing-rate-limiting">Implementing Rate Limiting</h2>
<p>Now, let's delve into implementing rate limiting within our Supabase-powered application. Rate limiting is essential to prevent abuse and ensure fair resource allocation. We achieve this through the <code>exceeded_rate_limit</code> and <code>check_rate_limit</code> functions.</p>
<h3 id="heading-the-exceededratelimit-function">The <code>exceeded_rate_limit</code> Function</h3>
<p>The <code>exceeded_rate_limit</code> function is responsible for checking if a client has exceeded the rate limit, which in this example is set at 5 requests per minute. Here's how it's defined:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> exceeded_rate_limit(ip_in <span class="hljs-built_in">TEXT</span>) 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">BOOLEAN</span> 
<span class="hljs-keyword">LANGUAGE</span> plpgsql <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
  request_count <span class="hljs-built_in">INTEGER</span>;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">count</span>(*) <span class="hljs-keyword">INTO</span> request_count 
  <span class="hljs-keyword">FROM</span> request_log 
  <span class="hljs-keyword">WHERE</span> ip = inet(ip_in) <span class="hljs-keyword">AND</span> <span class="hljs-built_in">timestamp</span> &gt; <span class="hljs-keyword">NOW</span>() - <span class="hljs-built_in">INTERVAL</span> <span class="hljs-string">'1 minute'</span>;

  RETURN request_count &gt;= 5; <span class="hljs-comment">-- limit of 5 requests per minute</span>
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<p>This function counts the number of requests made by a client within the last minute and returns <code>true</code> if the limit is exceeded.</p>
<h3 id="heading-the-checkratelimit-function">The <code>check_rate_limit</code> Function</h3>
<p>The <code>check_rate_limit</code> function is pivotal for enforcing rate limits. It effectively manages rate limiting by logging the current request using the <code>register_request</code> function and verifying if the rate limit has been surpassed. If the limit is exceeded, it raises an exception:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> check_rate_limit() 
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">VOID</span> 
<span class="hljs-keyword">LANGUAGE</span> plpgsql 
<span class="hljs-keyword">SET</span> search_path = <span class="hljs-keyword">public</span>, hdr, extensions
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span> 
  current_ip <span class="hljs-built_in">TEXT</span> := hdr.ip();
  request_method TEXT := current_setting('request.method', TRUE);
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-comment">-- Only log requests that are not GET or HEAD because they are run</span>
  <span class="hljs-comment">-- in read-only transactions</span>
  <span class="hljs-keyword">IF</span> request_method <span class="hljs-keyword">IS</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">OR</span> request_method <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">IN</span> (<span class="hljs-string">'GET'</span>, <span class="hljs-string">'HEAD'</span>) <span class="hljs-keyword">THEN</span>
    PERFORM register_request(current_ip);
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;

  <span class="hljs-comment">-- Check if the rate limit has been exceeded </span>
  <span class="hljs-comment">-- and raise an exception if necessary</span>
  IF exceeded_rate_limit(current_ip) THEN
    RAISE EXCEPTION 'Rate limit exceeded';
  <span class="hljs-keyword">END</span> <span class="hljs-keyword">IF</span>;
<span class="hljs-keyword">END</span>;
$$;
</code></pre>
<p>This function is a crucial component of your rate-limiting strategy, ensuring that each incoming request is correctly monitored and preventing clients from exceeding their allocated rate limits. It's important to note that this function primarily focuses on rate limiting for insert operations. While rate limiting for GET requests is possible, it may introduce performance concerns, such as making network requests that insert rate-limiting data.</p>
<h2 id="heading-configuring-pgheaderkit-with-postgrest">Configuring pg_headerkit with PostgREST</h2>
<p>To seamlessly integrate rate limiting with your Supabase-powered application, configure the <code>pgrst.db_pre_request</code> option to utilize the <code>check_rate_limit</code> function as a <a target="_blank" href="https://postgrest.org/en/stable/references/transactions.html#pre-request">pre-request</a> action within PostgREST:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">ROLE</span> authenticator 
<span class="hljs-keyword">SET</span> pgrst.db_pre_request = <span class="hljs-string">'check_rate_limit'</span>;
NOTIFY pgrst, 'reload config';
</code></pre>
<p>This configuration ensures that every request made to Supabase undergoes rate limit validation before execution, guaranteeing a fair and controlled usage of resources. Now, we can test the rate limit by sending a few post requests to a table:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1693688731283/82c0b0a8-7a04-4840-ac2f-584e79cf99a5.png" alt="Console log showing a few requests, then:{&quot;code&quot;:&quot;P0001&quot;,&quot;details&quot;:null,&quot;hint&quot;:null,&quot;message&quot;:&quot;Rate limit exceeded&quot;}%" class="image--center mx-auto" /></p>
<h2 id="heading-scheduled-cleanup">Scheduled Cleanup</h2>
<p>Maintaining the performance of your database requires periodic cleanup of old request logs. Schedule the <code>clean_old_requests</code> function to run automatically every midnight:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> cron.schedule(
  <span class="hljs-string">'clean_old_requests'</span>,
  <span class="hljs-string">'0 0 * * *'</span>, <span class="hljs-comment">-- Run every midnight</span>
  $$ <span class="hljs-keyword">SELECT</span> clean_old_requests(); $$
);
</code></pre>
<p>This automated cleanup process is crucial for keeping your database in an optimal state, free from unnecessary clutter, and ensuring efficient resource management.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this comprehensive blog post, we've delved into the intricacies of implementing rate limiting for your Supabase-powered applications. Leveraging the power of PostgreSQL and the versatile pg_headerkit extension, we've provided you with a step-by-step guide to ensure fair resource allocation and safeguard your application against abuse.</p>
<p>Rate limiting is a fundamental tool in your arsenal to maintain top-notch performance and deliver a consistently excellent user experience. Armed with the knowledge gained from this article, you're now well-prepared to seamlessly integrate rate limiting into your Supabase application.</p>
<p>Don't stop here; take the concepts discussed in this post and adapt them to your specific use cases. Experiment, explore, and fine-tune your rate-limiting strategy to perfectly align with your application's unique requirements.</p>
<p>If you found this article valuable, you might also be interested in exploring related topics:</p>
<ul>
<li><p><a target="_blank" href="https://blog.mansueli.com/how-to-boost-supabase-reliability-a-guide-to-using-postgres-foreign-data-wrappers">Boosting Supabase Reliability: A Guide to Using Postgres Foreign Data Wrappers</a>: Learn how to enhance the reliability of your Supabase applications.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql">Exploring Data Relationships with Supabase and PostgreSQL</a>: Dive deeper into understanding data relationships in Supabase.</p>
</li>
<li><p><a target="_blank" href="https://blog.mansueli.com/safeguarding-data-integrity-with-pg-safeupdate-in-postgresql-and-supabase">Safeguarding Data Integrity with pg-safeupdate in PostgreSQL and Supabase</a>: Explore techniques to maintain data integrity in your database.</p>
</li>
</ul>
<p>For further information and guidance, consider these valuable references. For any questions, feedback, or assistance, please don't hesitate to reach out to me. We're here to help you on your journey to mastering rate limiting in Supabase.</p>
<h2 id="heading-references">References</h2>
<p>For further information and guidance, consider these valuable references:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs">Supabase Official Documentation</a>: Explore Supabase's official documentation for in-depth insights into this powerful backend platform.</p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Official Documentation</a>: Dive into the official documentation of PostgreSQL, the robust database system at the core of Supabase.</p>
</li>
<li><p><a target="_blank" href="https://database.dev/burggraf/pg_headerkit">pg_headerkit Extension</a>: For more details on pg_headerkit, visit the official GitHub repository.</p>
</li>
<li><p><a target="_blank" href="https://postgrest.org/">PostgREST Official Documentation</a>: Discover PostgREST's documentation for additional information on this helpful tool.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Comprehensive Guide: Deploying and Debugging Custom Webhooks on Supabase & PostgreSQL]]></title><description><![CDATA[In today's dynamic landscape of cutting-edge application development, the role of automation has grown paramount, revolutionizing user experiences and operational efficiency. A domain where automation truly thrives is the seamless integration of webh...]]></description><link>https://blog.mansueli.com/automating-webhooks-supabase-postgresql-guide</link><guid isPermaLink="true">https://blog.mansueli.com/automating-webhooks-supabase-postgresql-guide</guid><category><![CDATA[webhooks]]></category><category><![CDATA[Databases]]></category><category><![CDATA[SQL]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 29 Aug 2023 13:31:29 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1693252346545/733c0010-7e5b-40f9-8d2e-5723d9b22bbc.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In today's dynamic landscape of cutting-edge application development, the role of automation has grown paramount, revolutionizing user experiences and operational efficiency. A domain where automation truly thrives is the seamless integration of webhooks into your application's architecture. Webhooks introduce the power to orchestrate actions triggered by specific events within your database. This automated responsiveness gains exceptional potency when harmonized with a versatile backend service like <a target="_blank" href="http://supabase.com/">Supabase</a> and the robust capabilities of PostgreSQL. This comprehensive article embarks on a journey to unveil the art of creating and debugging PostgreSQL webhooks using the Supabase platform, highlighting the potent synergy that can reshape and optimize your development workflow.</p>
<h2 id="heading-the-nexus-of-empowerment-an-alliance-of-supabase-and-postgresql">The Nexus of Empowerment: An Alliance of Supabase and PostgreSQL</h2>
<p>In the dynamic realm of modern application development, the selection of a backend service extends beyond technical considerations; it's a strategic choice with far-reaching implications for productivity and efficiency. The formidable partnership between Supabase and PostgreSQL presents a compelling proposition. This article delves into the realm of webhooks—automated triggers designed to spring into action in response to specific database events. Our journey involves an in-depth exploration of the intricacies involved in designing and debugging Postgres webhooks, harnessing the capabilities of Supabase to establish a fluid and efficient workflow.</p>
<h2 id="heading-expanding-horizons-the-webhook-wrapper-function">Expanding Horizons: The Webhook Wrapper Function</h2>
<p>At the heart of our webhook implementation, a pivotal role is played by the <code>request_wrapper</code> function. This versatile function operates as an intermediary, adroitly handling HTTP methods such as <code>GET</code>, <code>POST</code>, and <code>DELETE</code>. It forges smooth communication channels with external services and APIs, driven by dynamic input parameters encompassing <code>method</code>, <code>url</code>, <code>params</code>, <code>body</code>, and <code>headers</code>. Crafted using the expressive <code>plpgsql</code> language and fortified by the security definer concept for meticulous execution control, this function embodies the quintessence of streamlined automation.</p>
<h2 id="heading-taking-charge-of-your-automation-the-supabase-advantage">Taking Charge of Your Automation: The Supabase Advantage</h2>
<p>While Supabase generously offers webhooks as part of its toolkit, it is worth contemplating the merits of deploying them independently. By taking the reins of deployment, you gain the freedom to expand the timeout window, granting you greater control over the nature of the requests being executed. This strategic move allows you to tailor the automation to your specific needs, resulting in a more responsive and versatile system.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- </span>
<span class="hljs-comment">-- Webhook wrapper</span>
<span class="hljs-comment">--</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> request_wrapper(
    method <span class="hljs-built_in">TEXT</span>,
    <span class="hljs-keyword">url</span> <span class="hljs-built_in">TEXT</span>,
    params JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::JSONB,
    <span class="hljs-keyword">body</span> JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::JSONB,
    headers JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::JSONB
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-built_in">BIGINT</span>
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">SET</span> search_path = <span class="hljs-keyword">public</span>, extensions, net
<span class="hljs-keyword">LANGUAGE</span> plpgsql
<span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">DECLARE</span>
    request_id <span class="hljs-built_in">BIGINT</span>;
    timeout INT;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-keyword">timeout</span> := <span class="hljs-number">3000</span>;

    IF method = '<span class="hljs-keyword">DELETE</span><span class="hljs-string">' THEN
        SELECT net.http_delete(
            url:=url,
            params:=params,
            headers:=headers,
            timeout_milliseconds:=timeout
        ) INTO request_id;
    ELSIF method = '</span>POST<span class="hljs-string">' THEN
        SELECT net.http_post(
            url:=url,
            body:=body,
            params:=params,
            headers:=headers,
            timeout_milliseconds:=timeout
        ) INTO request_id;
    ELSIF method = '</span><span class="hljs-keyword">GET</span><span class="hljs-string">' THEN
        SELECT net.http_get(
            url:=url,
            params:=params,
            headers:=headers,
            timeout_milliseconds:=timeout
        ) INTO request_id;
    ELSE
        RAISE EXCEPTION '</span>Method must be <span class="hljs-keyword">DELETE</span>, POST, <span class="hljs-keyword">or</span> <span class="hljs-keyword">GET</span><span class="hljs-string">';
    END IF;
    RETURN request_id;
END;
$$;</span>
</code></pre>
<h2 id="heading-creating-a-trigger-function-for-automated-insert-events">Creating a Trigger Function for Automated Insert Events</h2>
<p>Imagine a scenario where a new row enters the picture in the <code>orders</code> table. This particular moment sets in motion an automatic process triggered by a special function we've named <code>after_order_insert()</code>. Within this function's code lies a sophisticated logic that effortlessly triggers a call to another function called <code>request_wrapper</code>. This dynamic function orchestrates a <code>POST</code> request that's directed to a specific webhook URL you define. The magic happens as the values from the newly added row come together to form a JSON package—an essential element that shapes the upcoming webhook request.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> after_order_insert()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
    PERFORM request_wrapper(<span class="hljs-string">'POST'</span>, <span class="hljs-string">'your_webhook_url_here'</span>, 
                            <span class="hljs-string">'{"order_id": '</span> || NEW.order_id || 
                            <span class="hljs-string">', "customer_id": '</span> || NEW.customer_id || 
                            <span class="hljs-string">', "total_amount": '</span> || NEW.total_amount || <span class="hljs-string">'}'</span>);
    RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql;
</code></pre>
<h2 id="heading-leading-the-way-with-triggers-powering-automation-for-new-inserts">Leading the Way with Triggers: Powering Automation for New Inserts</h2>
<p>In the dynamic realm of PostgreSQL databases, the birth of triggers stands as a foundational pillar of database event automation. Our carefully crafted trigger, given the name <code>orders_insert_webhook</code>, steps onto the stage. This conductor is designed to harmonize with the rhythm of each fresh row that finds its place in the revered <code>orders</code> table. This invisible maestro then coordinates the execution of the <code>after_order_insert()</code> function as soon as a new data insertion event occurs. The result is a symphony of seamless coordination, allowing webhooks to come to life in response to the emergence of new data.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> orders_insert_webhook
<span class="hljs-keyword">AFTER</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ON</span> orders
<span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
<span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> after_order_insert();
</code></pre>
<h2 id="heading-exploring-webhook-debugging-a-closer-look">Exploring Webhook Debugging: A Closer Look</h2>
<p>The journey through webhook integration entails not only implementation but also a vital debugging phase. This phase ensures the smooth functioning of your automated processes. At the core of this endeavor lies the <code>net._http_response</code> table—a valuable tool for debugging. This table captures the responses generated by HTTP requests executed through the <code>request_wrapper</code> function. It offers insights into request outcomes, including success, status codes, headers, content, timeouts, error messages, and creation timestamps.</p>
<h3 id="heading-navigating-the-debugging-process-inside-nethttpresponse">Navigating the Debugging Process: Inside <code>net._http_response</code></h3>
<p>In your PostgreSQL database architecture, the <code>net._http_response</code> table takes on a crucial role. Its design encapsulates essential debugging information, aiding you in refining your automated workflows. This table serves as a repository of records detailing each HTTP request's journey. These insights are invaluable for diagnosing and resolving any issues in your automation processes. From tracking response status and content type to examining headers, content, and possible timeouts, this table becomes your compass in the journey to create robust and efficient automation.</p>
<h3 id="heading-real-world-insight-practical-scenarios">Real-World Insight: Practical Scenarios</h3>
<p>To understand the practicality of the <code>net._http_response</code> table, let's consider an example where you've set up a webhook to notify customers upon order fulfillment. As you navigate this process, the <code>net._http_response</code> table becomes your ally.</p>
<p><strong>Use Case 1: Checking Request Status</strong></p>
<p>To verify request status, execute the following query:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>, status_code, timed_out
<span class="hljs-keyword">FROM</span> net._http_response;
</code></pre>
<p>This query offers a quick overview of request details, displaying IDs, status codes, and any timeouts.</p>
<p><strong>Use Case 2: Exploring Request Details</strong></p>
<p>For a comprehensive view of a specific request, use:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> *
<span class="hljs-keyword">FROM</span> net._http_response
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = your_request_id_here;
</code></pre>
<p>Replace <code>your_request_id_here</code> with the actual ID of the request. This query provides a detailed examination, including headers, content, and error messages.</p>
<p>In the process of webhook debugging, the <code>net._http_response</code> table shines as a guiding light. Its insights empower you to craft and maintain automation processes that endure.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Example: Checking request status</span>
<span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">id</span>, status_code, timed_out
<span class="hljs-keyword">FROM</span> net._http_response;

<span class="hljs-comment">-- Example: Exploring request details</span>
<span class="hljs-keyword">SELECT</span> *
<span class="hljs-keyword">FROM</span> net._http_response
<span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = your_request_id_here;
</code></pre>
<h2 id="heading-wrapping-up-your-path-to-smarter-automation">Wrapping Up: Your Path to Smarter Automation</h2>
<p>As we come to the end of this exploration into the world of automation and efficiency in app development, it's time to reflect on the journey we've taken. Throughout this article, we've teamed up Supabase and PostgreSQL with webhooks—a powerful trio that empowers you to create smarter, more responsive applications.</p>
<p>If you're hungry for more insights and want to delve deeper into enhancing your Supabase-powered apps, don't forget to check out some of our previous articles:</p>
<ul>
<li><p>Wondering how to make your Supabase setup even more reliable? Dive into our guide on <a target="_blank" href="https://blog.mansueli.com/how-to-boost-supabase-reliability-a-guide-to-using-postgres-foreign-data-wrappers">Boosting Supabase Reliability with Postgres Foreign Data Wrappers</a>.</p>
</li>
<li><p>Exploring data relationships in Supabase and PostgreSQL? Learn the ropes with our article on <a target="_blank" href="https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql">Exploring Data Relationships with Supabase and PostgreSQL</a>.</p>
</li>
<li><p>Need to empower your users with secure password verification? Our insights on <a target="_blank" href="https://blog.mansueli.com/secure-password-verification-and-update-with-supabase-and-postgresql">Secure Password Verification and Update with Supabase and PostgreSQL</a> have got you covered.</p>
</li>
</ul>
<p>With these resources at your fingertips, you're well-equipped to take your app development journey to new heights. Remember, the world of coding is full of possibilities, and webhooks are just the beginning. Keep innovating, keep learning, and keep coding!</p>
<p>Got any thoughts or questions? We'd love to hear from you. Drop a comment below and let's keep the conversation going!</p>
]]></content:encoded></item><item><title><![CDATA[Creating Customized i18n-Ready Authentication Emails using Supabase Edge Functions, PostgreSQL, and Resend]]></title><description><![CDATA[Welcome to another exciting tutorial on building robust applications! Today, we're delving into the world of Supabase, an open-source backend solution that equips developers with a powerful toolkit for creating scalable applications. In this guide, w...]]></description><link>https://blog.mansueli.com/creating-customized-i18n-ready-authentication-emails-using-supabase-edge-functions-postgresql-and-resend</link><guid isPermaLink="true">https://blog.mansueli.com/creating-customized-i18n-ready-authentication-emails-using-supabase-edge-functions-postgresql-and-resend</guid><category><![CDATA[authentication]]></category><category><![CDATA[i18n]]></category><category><![CDATA[supabase]]></category><category><![CDATA[resend]]></category><category><![CDATA[email templates]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 22 Aug 2023 13:45:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692638149230/a75fa8b7-48d8-4010-8404-9f041c7299d6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to another exciting tutorial on building robust applications! Today, we're delving into the world of Supabase, an open-source backend solution that equips developers with a powerful toolkit for creating scalable applications. In this guide, we'll explore how to harness the potential of <a target="_blank" href="https://supabase.com/docs/guides/functions">Supabase Edge Functions</a>, PostgreSQL, and the Resend email service to craft a personalized authentication email system.</p>
<h2 id="heading-introduction">Introduction</h2>
<p>As the demand for efficient backend solutions continues to grow, Supabase has emerged as a game-changer. Offering features like real-time subscriptions, authentication, and database management, Supabase simplifies complex backend tasks. However, one unique challenge lies in customizing email templates for user engagement. In our previous blog post on <a target="_blank" href="https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql">Exploring Data Relationships with Supabase and PostgreSQL</a>, we delved into the intricacies of data relationships. Building upon that foundation, we now tackle the challenge of creating a personalized authentication email system that also seamlessly supports i18n localization.</p>
<h2 id="heading-the-significance-of-i18n-localization-in-emails">The Significance of i18n Localization in Emails</h2>
<p>In today's global landscape, catering to diverse audiences is imperative for enhancing user engagement. Internationalization (i18n) ensures that emails resonate with users across various cultures and languages. By dynamically replacing content based on the user's preferred language, we not only enhance the user experience but also foster a sense of inclusivity.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before we dive into the technical intricacies, it's essential to be well-acquainted with Supabase, PostgreSQL, and have a grasp of the basics of Deno if necessary. Setting up your development environment, installing essential tools, and configuring dependencies based on your operating system will establish a solid foundation for a successful implementation.</p>
<h2 id="heading-establishing-a-solid-database-foundation">Establishing a Solid Database Foundation</h2>
<p>Our journey begins with the establishment of a robust database foundation. This involves the creation of an email_templates table within an internal schema. This table serves as a pivotal repository, storing vital information such as subject lines, content, languages, and template types.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SCHEMA</span> internal;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> internal.email_templates (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">BIGINT</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">BY</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span>,
  subject <span class="hljs-built_in">TEXT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">content</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-literal">NULL</span>,
  email_language <span class="hljs-built_in">TEXT</span> <span class="hljs-literal">NULL</span>,
  email_type <span class="hljs-built_in">TEXT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">CONSTRAINT</span> email_templates_pkey PRIMARY <span class="hljs-keyword">KEY</span> (<span class="hljs-keyword">id</span>)
);
</code></pre>
<p>By crafting this strong foundation, we lay the groundwork for a dynamic and versatile email system. This system will be powered by the forthcoming get_email_template function, which we'll explore in the following sections.</p>
<h2 id="heading-crafting-the-dynamic-getemailtemplate-function">Crafting the Dynamic <code>get_email_template</code> Function</h2>
<p>The core of our email system resides within the get_email_template function. This dynamic function retrieves email templates, utilizing inputs such as the template type, link, and language. Importantly, the function seamlessly integrates with the email_templates table, providing personalized email content.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> get_email_template(
  template_type <span class="hljs-built_in">TEXT</span>,
  <span class="hljs-keyword">link</span> <span class="hljs-built_in">TEXT</span>,
  <span class="hljs-keyword">language</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'en'</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">JSON</span>
<span class="hljs-keyword">SECURITY</span> DEFINER
<span class="hljs-keyword">SET</span> search_path = <span class="hljs-keyword">public</span>, internal <span class="hljs-keyword">AS</span>
$<span class="hljs-keyword">BODY</span>$
<span class="hljs-keyword">DECLARE</span>
  email_subject <span class="hljs-built_in">TEXT</span>;
  email_content TEXT;
  email_json JSON;
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">SELECT</span> subject, <span class="hljs-keyword">REPLACE</span>(<span class="hljs-keyword">content</span>, <span class="hljs-string">'{{LINK}}'</span>, <span class="hljs-keyword">link</span>) <span class="hljs-keyword">INTO</span> email_subject, email_content
  <span class="hljs-keyword">FROM</span> internal.email_templates
  <span class="hljs-keyword">WHERE</span> email_type = template_type <span class="hljs-keyword">AND</span> email_language = <span class="hljs-keyword">language</span>;
  email_json := json_build_object('subject', email_subject, 'content', email_content);
  RETURN email_json;
<span class="hljs-keyword">END</span>;
$BODY$
LANGUAGE plpgsql;
<span class="hljs-comment">-- Protect this function to be only available to service_role key:</span>
<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> get_email_template <span class="hljs-keyword">from</span> <span class="hljs-keyword">public</span>;
<span class="hljs-keyword">REVOKE</span> <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">FUNCTION</span> get_email_template <span class="hljs-keyword">FROM</span> anon, <span class="hljs-keyword">authenticated</span>;
</code></pre>
<h2 id="heading-customizing-email-templates-for-common-authentication-scenarios">Customizing Email Templates for Common Authentication Scenarios</h2>
<p>To enhance user experience and engagement, we'll be customizing email templates for several common authentication scenarios: password recovery, signup confirmation, invitation, and magic link.</p>
<p>These templates will be available in three languages: Portuguese, English, and Danish. You can seamlessly integrate these templates into the <code>email_templates</code> table within the internal schema. Below, you'll find the templates for each scenario:</p>
<pre><code class="lang-sql"><span class="hljs-comment">--</span>
<span class="hljs-comment">-- Data for Name: email_templates; Type: TABLE DATA; Schema: internal; Owner: postgres</span>
<span class="hljs-comment">--</span>

<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> <span class="hljs-string">"internal"</span>.<span class="hljs-string">"email_templates"</span> (<span class="hljs-string">"id"</span>, <span class="hljs-string">"subject"</span>, <span class="hljs-string">"content"</span>, <span class="hljs-string">"email_language"</span>, <span class="hljs-string">"email_type"</span>) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-number">1</span>, <span class="hljs-string">'Din Magisk Link'</span>, <span class="hljs-string">'&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt;
&lt;meta http-equiv="Content-Type" content="text/html charset=UTF-8" /&gt;
&lt;html lang="da"&gt;
&lt;head&gt;&lt;/head&gt;
&lt;body style="background-color:#ffffff;font-family:-apple-system,BlinkMacSystemFont,&amp;quot;Segoe UI&amp;quot;,Roboto,Oxygen-Sans,Ubuntu,Cantarell,&amp;quot;Helvetica Neue&amp;quot;,sans-serif"&gt;
    &lt;table align="center" role="presentation" cellSpacing="0" cellPadding="0" border="0" width="100%" style="max-width:37.5em;margin:0 auto;padding:20px 25px 48px;background-image:url(&amp;quot;/assets/background-image.png&amp;quot;);background-position:bottom;background-repeat:no-repeat, no-repeat"&gt;
        &lt;tr style="width:100%"&gt;
            &lt;td&gt;
                &lt;h1 style="font-size:28px;font-weight:bold;margin-top:48px"&gt;🪄 Din magiske link&lt;/h1&gt;
                &lt;table style="margin:24px 0" align="center" border="0" cellPadding="0" cellSpacing="0" role="presentation" width="100%"&gt;
                    &lt;tbody&gt;
                        &lt;tr&gt;
                            &lt;td&gt;
                                &lt;p style="font-size:16px;line-height:26px;margin:16px 0"&gt;&lt;a target="_blank" style="color:#FF6363;text-decoration:none" href="{{LINK}}"&gt;👉 Klik her for at logge ind 👈&lt;/a&gt;&lt;/p&gt;
                                &lt;p style="font-size:16px;line-height:26px;margin:16px 0"&gt;Hvis du ikke har anmodet om dette, bedes du ignorere denne e-mail.&lt;/p&gt;
                            &lt;/td&gt;
                        &lt;/tr&gt;
                    &lt;/tbody&gt;
                &lt;/table&gt;
                &lt;p style="font-size:16px;line-height:26px;margin:16px 0"&gt;Bedste hilsner,&lt;br /&gt;- Contoso Team&lt;/p&gt;
                &lt;hr style="width:100%;border:none;border-top:1px solid #eaeaea;border-color:#dddddd;margin-top:48px" /&gt;
                &lt;p style="font-size:12px;line-height:24px;margin:16px 0;color:#8898aa;margin-left:4px"&gt;Contoso Technologies Inc.&lt;/p&gt;
            &lt;/td&gt;
        &lt;/tr&gt;
    &lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;
'</span>, <span class="hljs-string">'da'</span>, <span class="hljs-string">'magiclink'</span>);

<span class="hljs-comment">-- Check the full SQL file and all templates in the GitHub repo</span>
<span class="hljs-comment">-- https://github.com/mansueli/Supabase-Edge-AuthMailer</span>
</code></pre>
<p>By offering tailored templates for these scenarios in multiple languages, you ensure that your email communications resonate with a broader audience. This personalization fosters stronger user engagement and interaction.</p>
<h2 id="heading-developing-the-deno-edge-function-indexts">Developing the Deno Edge Function (index.ts)</h2>
<p>In the upcoming section, we'll delve into the development process of the Deno Edge Function responsible for managing authentication email requests. Below, we'll provide an overview of the pivotal steps involved in this process:</p>
<ol>
<li><p><strong>Initializing Imports and Constants:</strong> We'll commence by importing essential modules and setting up constants that will facilitate the seamless development process.</p>
</li>
<li><p><strong>Creating a Secure Supabase Client:</strong> Learn how to establish a secure connection to Supabase using admin credentials, ensuring authorized access to the required resources.</p>
</li>
<li><p><strong>Handling Incoming HTTP Requests:</strong> Discover the utilization of the <code>serve</code> function to effectively handle incoming HTTP requests, enhancing the overall responsiveness of your system.</p>
</li>
<li><p><strong>Extracting Vital Parameters:</strong> Understand the process of extracting crucial parameters from incoming requests, such as email, authentication type, language, password, and redirection URL.</p>
</li>
<li><p><strong>Generating Secure Authentication Links:</strong> Explore the steps involved in generating secure authentication links using the Supabase admin API, facilitating secure user interactions.</p>
</li>
<li><p><strong>Customizing Redirection Links:</strong> Learn how to customize redirection links to match specific requirements and elevate the user experience.</p>
</li>
<li><p><strong>Invoking</strong> <code>get_email_template</code> Function: Dive into the usage of the Supabase <code>rpc</code> method to invoke the <code>get_email_template</code> function, enabling seamless retrieval of email content.</p>
</li>
<li><p><strong>Integration of Resend API:</strong> Understand the seamless integration of the Resend API, allowing the delivery of personalized and informative emails to users.</p>
</li>
</ol>
<p>For the complete code of the Deno Edge Function, refer to the provided <code>index.ts</code> file.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Importing required libraries</span>
<span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/std@0.192.0/http/server.ts'</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://esm.sh/@supabase/supabase-js@2'</span>

<span class="hljs-comment">// Defining CORS headers</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> corsHeaders = {
  <span class="hljs-string">'Access-Control-Allow-Origin'</span>: <span class="hljs-string">'*'</span>,
  <span class="hljs-string">'Access-Control-Allow-Headers'</span>: <span class="hljs-string">'authorization, x-client-info, apikey, content-type'</span>,
}

<span class="hljs-comment">// Log to indicate the function is up and running</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Function "auth-mailer" up and running!`</span>)

<span class="hljs-comment">// Creating a Supabase client using environment variables</span>
<span class="hljs-keyword">const</span> supabaseAdmin = createClient(
  Deno.env.get(<span class="hljs-string">'SUPABASE_URL'</span>) ?? <span class="hljs-string">''</span>,
  Deno.env.get(<span class="hljs-string">'SUPABASE_SERVICE_ROLE_KEY'</span>) ?? <span class="hljs-string">''</span>
)

<span class="hljs-comment">// Define a server that handles different types of requests</span>
serve(<span class="hljs-keyword">async</span> (req: Request) =&gt; {
  <span class="hljs-comment">// Handling preflight CORS requests</span>
  <span class="hljs-keyword">if</span> (req.method === <span class="hljs-string">'OPTIONS'</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>, { headers: corsHeaders })
  }

  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Destructuring request JSON and setting default values</span>
    <span class="hljs-keyword">let</span> { email, <span class="hljs-keyword">type</span>, language = <span class="hljs-string">'en'</span>, password = <span class="hljs-string">''</span>, redirect_to = <span class="hljs-string">''</span> } = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify({ email, <span class="hljs-keyword">type</span>, language, password }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));

    <span class="hljs-comment">// Generate a link with admin API call</span>
    <span class="hljs-keyword">let</span> linkPayload: <span class="hljs-built_in">any</span> = {
      <span class="hljs-keyword">type</span>,
      email,
    }

    <span class="hljs-comment">// If type is 'signup', add password to the payload</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">type</span> == <span class="hljs-string">'signup'</span>) {
      linkPayload = {
        ...linkPayload,
        password,
      }
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"linkPayload"</span>, linkPayload);
    }

    <span class="hljs-comment">// Generate the link</span>
    <span class="hljs-keyword">const</span> { data: linkResponse, error: linkError } = <span class="hljs-keyword">await</span> supabaseAdmin.auth.admin.generateLink(linkPayload)
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"linkResponse"</span>, linkResponse);

    <span class="hljs-comment">// Throw error if any occurs during link generation</span>
    <span class="hljs-keyword">if</span> (linkError) {
      <span class="hljs-keyword">throw</span> linkError;
    }

    <span class="hljs-comment">// Getting the actual link and manipulating the redirect link</span>
    <span class="hljs-keyword">let</span> actual_link = linkResponse.properties.action_link;
    <span class="hljs-keyword">if</span> (redirect_to != <span class="hljs-string">''</span>) {
      actual_link = actual_link.split(<span class="hljs-string">'redirect_to='</span>)[<span class="hljs-number">0</span>];
      actual_link = actual_link + <span class="hljs-string">'&amp;redirect_to='</span> + redirect_to;
    }

    <span class="hljs-comment">// Log the template data</span>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify({ <span class="hljs-string">"template_type"</span>:<span class="hljs-keyword">type</span>, <span class="hljs-string">"link"</span>: linkResponse, <span class="hljs-string">"language"</span>:language }, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));

    <span class="hljs-comment">// Get the email template</span>
    <span class="hljs-keyword">const</span> { data: templateData, error: templateError } = <span class="hljs-keyword">await</span> supabaseAdmin.rpc(<span class="hljs-string">'get_email_template'</span>, { <span class="hljs-string">"template_type"</span>:<span class="hljs-keyword">type</span>, <span class="hljs-string">"link"</span>: actual_link, <span class="hljs-string">"language"</span>:language });

    <span class="hljs-comment">// Throw error if any occurs during template fetching</span>
    <span class="hljs-keyword">if</span> (templateError) {
      <span class="hljs-keyword">throw</span> templateError;
    }

    <span class="hljs-comment">// Send the email using resend</span>
    <span class="hljs-keyword">const</span> RESEND_API_KEY = Deno.env.get(<span class="hljs-string">'RESEND_API_KEY'</span>)
    <span class="hljs-keyword">const</span> resendRes = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'https://api.resend.com/emails'</span>, {
      method: <span class="hljs-string">'POST'</span>,
      headers: {
        <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
        Authorization: <span class="hljs-string">`Bearer <span class="hljs-subst">${RESEND_API_KEY}</span>`</span>,
      },
      body: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-keyword">from</span>: <span class="hljs-string">'rodrigo@mansueli.com'</span>,
        to: email,
        subject: templateData.subject,
        html: templateData.content,
      }),
    });

    <span class="hljs-comment">// Handle the response from the resend request</span>
    <span class="hljs-keyword">const</span> resendData = <span class="hljs-keyword">await</span> resendRes.json();
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify(resendData), {
      status: resendRes.status,
      headers: {
        <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
      },
    })
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-comment">// Handle any other errors</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: error.message }), {
      headers: { ...corsHeaders, <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
      status: <span class="hljs-number">400</span>,
    })
  }
})
</code></pre>
<h2 id="heading-addressing-supabase-email-template-behavior">Addressing Supabase Email Template Behavior</h2>
<p>It's essential to acknowledge a particular behavior of Supabase when handling email templates. By default, Supabase removes HTML tags from email templates when using the platform's default templates. The approach we present here allows you to exercise control over the visual presentation of your email content. To effectively include HTML elements in your email templates, you can adhere to standard HTML practices.</p>
<h2 id="heading-conclusion-and-future-enhancements">Conclusion and Future Enhancements</h2>
<p>Congratulations on successfully constructing a personalized authentication email system using Supabase Edge Functions, PostgreSQL, and the Resend service. By harnessing the capabilities of Supabase, you've streamlined the process of delivering tailored authentication emails to your users.</p>
<p>While this tutorial covers the foundational aspects, there's always room for growth. Consider expanding the range of email template customization or integrating additional third-party services to elevate user engagement. As you continue exploring Supabase's capabilities, share your insights and enhancements with the open-source community.</p>
<p>You can find the complete code used in this article on <a target="_blank" href="https://github.com/mansueli/Supabase-Edge-AuthMailer">GitHub</a>.</p>
<h2 id="heading-further-learning-resources">Further Learning Resources</h2>
<p>For additional learning, we recommend exploring the following resources:</p>
<ul>
<li><p><a target="_blank" href="https://supabase.io/docs">Official Supabase Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://www.postgresql.org/docs/">PostgreSQL Documentation</a></p>
</li>
<li><p><a target="_blank" href="https://deno.land/">Deno Official Website</a></p>
</li>
<li><p><a target="_blank" href="https://resend.com/docs">Resend API Documentation</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to Boost Supabase: A Guide to Using Postgres Foreign Data Wrappers]]></title><description><![CDATA[Welcome to this comprehensive guide on enhancing the reliability and isolation of your Supabase project using the power of Foreign Data Wrappers (FDW). In this blog post, we will delve into the strategic implementation of FDWs to connect your project...]]></description><link>https://blog.mansueli.com/how-to-boost-supabase-a-guide-to-using-postgres-foreign-data-wrappers</link><guid isPermaLink="true">https://blog.mansueli.com/how-to-boost-supabase-a-guide-to-using-postgres-foreign-data-wrappers</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[fdw]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 15 Aug 2023 14:50:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1692110853459/cb4bbb06-3e08-4a8d-94b8-431086b95489.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Welcome to this comprehensive guide on enhancing the reliability and isolation of your Supabase project using the power of Foreign Data Wrappers (FDW). In this blog post, we will delve into the strategic implementation of FDWs to connect your project with an independent queue system. By adopting this approach, your Supabase project gains an additional layer of resilience, ensuring that even if your primary system encounters issues, the queue continues to operate seamlessly. This exploration promises to equip you with the knowledge to fortify your application's robustness. In a previous post, we explored <a target="_blank" href="https://blog.mansueli.com/mysql-foreign-data-wrapper-supabase-postgresql-edge-functions">how to create a pseudo FDW to MySQL</a> using an Edge Function as a bridge, now we'll explore using proper Postgres FDW.</p>
<h3 id="heading-prerequisites-for-implementing-fdws"><strong>Prerequisites for Implementing FDWs</strong></h3>
<p>To embark on this journey, it's essential to have the following prerequisites in place.</p>
<ol>
<li><p><strong>Main Supabase Project:</strong> You should have your primary Supabase project set up and operational. This serves as the foundation upon which we'll build the enhanced system.</p>
</li>
<li><p><strong>Foreign Supabase Project:</strong> Similarly, your separate foreign Supabase project needs to be ready. This distinct database will be integrated using the Foreign Data Wrappers approach.</p>
</li>
</ol>
<p>Before proceeding, ensure these prerequisites are aligned to make the most of this guide.</p>
<p><strong>Exploring the Process:</strong> The process we're about to undertake involves creating a bridge between your main Supabase project and the foreign Supabase project through the magic of Foreign Data Wrappers. This bridge facilitates communication and data sharing between the projects, offering enhanced reliability and isolation.</p>
<h3 id="heading-create-a-user-in-the-foreign-database"><strong>Create a User in the Foreign Database:</strong></h3>
<p>In this crucial step, we lay the foundation for secure communication. By creating a dedicated user 'foreign_user' and granting it specific privileges, we establish the necessary credentials to bridge the databases. The use of BYPASSRLS ensures controlled data access.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> foreign_user <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">PASSWORD</span> <span class="hljs-string">'password'</span> BYPASSRLS;

<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">USAGE</span> 
<span class="hljs-keyword">ON</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> 
<span class="hljs-keyword">TO</span> foreign_user;

<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">SELECT</span>, <span class="hljs-keyword">INSERT</span>, <span class="hljs-keyword">UPDATE</span>, <span class="hljs-keyword">DELETE</span> 
<span class="hljs-keyword">ON</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">TABLES</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> 
<span class="hljs-keyword">TO</span> foreign_user;

<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">USAGE</span>, <span class="hljs-keyword">SELECT</span> 
<span class="hljs-keyword">ON</span> <span class="hljs-keyword">ALL</span> SEQUENCES <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> <span class="hljs-keyword">public</span> 
<span class="hljs-keyword">TO</span> foreign_user;
</code></pre>
<h3 id="heading-configuration-in-the-main-database"><strong>Configuration in the Main Database:</strong></h3>
<p>This step involves configuring your main Supabase database to communicate with the foreign server. We set up a new schema 'queue' to house the imported foreign data. Additionally, we establish a foreign server named 'foreign_server,' specifying connection details like host, port, and dbname. The user mapping ensures secure authentication, while the IMPORT FOREIGN SCHEMA command integrates the foreign schema into your main database.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> EXTENSION postgres_fdw;
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SCHEMA</span> queue;

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">PRIVILEGES</span> <span class="hljs-keyword">IN</span> <span class="hljs-keyword">SCHEMA</span> queue
<span class="hljs-keyword">GRANT</span> <span class="hljs-keyword">ALL</span> <span class="hljs-keyword">ON</span> <span class="hljs-keyword">TABLES</span> <span class="hljs-keyword">TO</span> postgres;

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">SERVER</span> foreign_server
    <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">DATA</span> WRAPPER postgres_fdw
    OPTIONS (host <span class="hljs-string">'db.foreign.supabase.co'</span>, port <span class="hljs-string">'5432'</span>, dbname <span class="hljs-string">'postgres'</span>);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">USER</span> <span class="hljs-keyword">MAPPING</span> <span class="hljs-keyword">FOR</span> postgres
    <span class="hljs-keyword">SERVER</span> foreign_server
    OPTIONS (<span class="hljs-keyword">user</span> <span class="hljs-string">'foreign_user'</span>, <span class="hljs-keyword">password</span> <span class="hljs-string">'password'</span>);

IMPORT FOREIGN SCHEMA public
    FROM SERVER foreign_server
    INTO queue;

<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">SERVER</span> foreign_server 
    OPTIONS (fetch_size <span class="hljs-string">'50000'</span>);
</code></pre>
<p><strong>Explaining fetch_size Adjustment:</strong> The <code>ALTER SERVER</code> command with the <code>fetch_size</code> option plays a vital role in optimizing data retrieval. By setting the fetch size to '50000,' we dictate how many rows of data the server fetches in a single request. Larger fetch sizes can enhance data retrieval efficiency, especially when dealing with substantial datasets.</p>
<p><strong>Enhancing Queue Reliability:</strong> This approach not only boosts the reliability of your main Supabase project but also increases the robustness of your queue system. To understand more about creating a queue system using Supabase and PostgreSQL, refer to the article <a target="_blank" href="https://blog.mansueli.com/building-a-queue-system-with-supabase-and-postgresql">Building a Queue System with Supabase and PostgreSQL</a>.</p>
<h3 id="heading-conclusion"><strong>Conclusion</strong></h3>
<p>By meticulously following these steps, you pave the way for a robust, interconnected Supabase ecosystem. The integration of Foreign Data Wrappers fosters reliability, ensuring that the queue system remains resilient even in the face of challenges.</p>
<p>Don't hesitate to explore the <a target="_blank" href="https://supabase.com/docs/guides/database/extensions">Supabase documentation</a>, which offers a wealth of knowledge on database extensions and their applications.</p>
]]></content:encoded></item><item><title><![CDATA[Exploring Data Relationships with Supabase and PostgreSQL]]></title><description><![CDATA[Discover Effective Data Relationship Management with Supabase Join and Inner Join Techniques.
Supabase, the potent open-source platform, brings you real-time and secure backend-as-a-service capabilities, leveraging the power of PostgreSQL. Uncover th...]]></description><link>https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql</link><guid isPermaLink="true">https://blog.mansueli.com/exploring-data-relationships-with-supabase-and-postgresql</guid><category><![CDATA[SQL]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 08 Aug 2023 13:51:11 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1691501802483/645c7277-621c-413f-bc08-64d742430652.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Discover Effective Data Relationship Management with Supabase Join and Inner Join Techniques.</p>
<p>Supabase, the potent open-source platform, brings you real-time and secure backend-as-a-service capabilities, leveraging the power of PostgreSQL. Uncover the art of managing data relationships effectively using Supabase and PostgreSQL.</p>
<h2 id="heading-utilizing-foreign-keys-to-establish-table-links">Utilizing Foreign Keys to Establish Table Links</h2>
<p>The foundation of dynamic and efficient applications lies in robust and interconnected databases. One crucial technique for achieving this is harnessing the potential of foreign keys to establish relationships between database tables. Let's delve into how you can leverage foreign keys for linking tables and executing advanced JOIN queries.</p>
<h3 id="heading-creating-tables-for-demonstrating-join-queries">Creating Tables for Demonstrating JOIN Queries</h3>
<p>Before we plunge into data relationships, let's kick things off by setting up the required database tables. Our example involves five distinct tables: <code>books</code>, <code>publishers</code>, <code>translations</code>, <code>teachers</code>, and <code>courses</code>. Each table serves a unique purpose in our application. For instance, the <code>books</code> table stores book details, the <code>publishers</code> table holds publisher information, and the <code>translations</code> table tracks book translations.</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> books (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">ALWAYS</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    title <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    author <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> publishers (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">ALWAYS</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    book_id <span class="hljs-built_in">INTEGER</span>,
    <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    published_date <span class="hljs-built_in">DATE</span>,
    <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (book_id) <span class="hljs-keyword">REFERENCES</span> books(<span class="hljs-keyword">id</span>)
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> translations (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">ALWAYS</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    book_id <span class="hljs-built_in">INTEGER</span>,
    <span class="hljs-keyword">language</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
    translator <span class="hljs-built_in">TEXT</span>,
    <span class="hljs-keyword">FOREIGN</span> <span class="hljs-keyword">KEY</span> (book_id) <span class="hljs-keyword">REFERENCES</span> books(<span class="hljs-keyword">id</span>)
);
</code></pre>
<p>Here's a glimpse of example insert statements:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> books (title, author) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-string">'The Great Gatsby'</span>, <span class="hljs-string">'F. Scott Fitzgerald'</span>),
    (<span class="hljs-string">'To Kill a Mockingbird'</span>, <span class="hljs-string">'Harper Lee'</span>),
    (<span class="hljs-string">'1984'</span>, <span class="hljs-string">'George Orwell'</span>),
    (<span class="hljs-string">'Pride and Prejudice'</span>, <span class="hljs-string">'Jane Austen'</span>),
    (<span class="hljs-string">'The Hobbit'</span>, <span class="hljs-string">'J.R.R. Tolkien'</span>),
    (<span class="hljs-string">'Harry Potter and the Sorcerer''s Stone'</span>, <span class="hljs-string">'J.K. Rowling'</span>),
    (<span class="hljs-string">'The Catcher in the Rye'</span>, <span class="hljs-string">'J.D. Salinger'</span>),
    (<span class="hljs-string">'Lord of the Flies'</span>, <span class="hljs-string">'William Golding'</span>),
    (<span class="hljs-string">'The Chronicles of Narnia'</span>, <span class="hljs-string">'C.S. Lewis'</span>),
    (<span class="hljs-string">'Brave New World'</span>, <span class="hljs-string">'Aldous Huxley'</span>);
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> publishers (book_id, <span class="hljs-keyword">name</span>, published_date) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-number">1</span>, <span class="hljs-string">'Scribner'</span>, <span class="hljs-string">'1925-04-10'</span>),
    (<span class="hljs-number">2</span>, <span class="hljs-string">'HarperCollins'</span>, <span class="hljs-string">'1960-07-11'</span>),
    (<span class="hljs-number">3</span>, <span class="hljs-string">'Secker &amp; Warburg'</span>, <span class="hljs-string">'1949-06-08'</span>),
    (<span class="hljs-number">4</span>, <span class="hljs-string">'T. Egerton, Whitehall'</span>, <span class="hljs-string">'1813-01-28'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Allen &amp; Unwin'</span>, <span class="hljs-string">'1937-09-21'</span>),
    (<span class="hljs-number">6</span>, <span class="hljs-string">'Bloomsbury'</span>, <span class="hljs-string">'1997-06-26'</span>),
    (<span class="hljs-number">7</span>, <span class="hljs-string">'Little, Brown'</span>, <span class="hljs-string">'1951-07-16'</span>),
    (<span class="hljs-number">8</span>, <span class="hljs-string">'Faber and Faber'</span>, <span class="hljs-string">'1954-09-17'</span>),
    (<span class="hljs-number">9</span>, <span class="hljs-string">'Geoffrey Bles'</span>, <span class="hljs-string">'1950-10-16'</span>),
    (<span class="hljs-number">10</span>, <span class="hljs-string">'Chatto &amp; Windus'</span>, <span class="hljs-string">'1932-06-23'</span>);
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> translations (book_id, <span class="hljs-keyword">language</span>, translator) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-number">1</span>, <span class="hljs-string">'French'</span>, <span class="hljs-string">'Maurice Ravel'</span>),
    (<span class="hljs-number">1</span>, <span class="hljs-string">'German'</span>, <span class="hljs-string">'Franz Kafka'</span>),
    (<span class="hljs-number">2</span>, <span class="hljs-string">'Spanish'</span>, <span class="hljs-string">'Gabriel García Márquez'</span>),
    (<span class="hljs-number">2</span>, <span class="hljs-string">'Italian'</span>, <span class="hljs-string">'Italo Calvino'</span>),
    (<span class="hljs-number">3</span>, <span class="hljs-string">'Russian'</span>, <span class="hljs-string">'Yevgeny Zamyatin'</span>),
    (<span class="hljs-number">3</span>, <span class="hljs-string">'Chinese'</span>, <span class="hljs-string">'Lu Xun'</span>),
    (<span class="hljs-number">4</span>, <span class="hljs-string">'Japanese'</span>, <span class="hljs-string">'Haruki Murakami'</span>),
    (<span class="hljs-number">4</span>, <span class="hljs-string">'Korean'</span>, <span class="hljs-string">'Shin Kyung-sook'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Spanish'</span>, <span class="hljs-string">'Jorge Luis Borges'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Arabic'</span>, <span class="hljs-string">'Naguib Mahfouz'</span>);
</code></pre>
<h3 id="heading-performing-join-queries-using-supabase-js">Performing JOIN Queries Using Supabase-JS</h3>
<p>Unlock the ability to combine data from multiple tables with inner joins, using the Supabase-JS JavaScript library. This user-friendly tool simplifies database querying and inner join operations, ensuring a seamless experience.</p>
<p>To seamlessly perform an inner join using Supabase-JS, leverage the <code>select</code> method while specifying the desired columns for retrieval. Consult the <a target="_blank" href="https://supabase.io/docs/guides/database/select">Supabase documentation</a> for comprehensive guidance on working with Supabase-JS.</p>
<p>With the groundwork laid for data relationships through foreign keys, let's delve into practical examples of executing JOIN queries using Supabase-JS.</p>
<h3 id="heading-basic-join-operation-connecting-books-and-publishers-tables">Basic JOIN Operation: Connecting Books and Publishers Tables</h3>
<p>Here's a fundamental JOIN operation between the <code>books</code> and <code>publishers</code> tables with this JavaScript code snippet:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692645687933/070a42a6-366f-4a5b-975f-985960fccac1.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase  
      .from(<span class="hljs-string">'books'</span>)
      .select(<span class="hljs-string">'*,publishers(*)'</span>);
<span class="hljs-comment">/* Example response:
[
  {
    "id": 1,
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "publishers": [
      {
        "id": 1,
        "book_id": 1,
        "name": "Scribner",
        "published_date": "1925-04-10"
      }
    ]
  },
  {
    "id": 2,
    "title": "To Kill a Mockingbird",
    "author": "Harper Lee",
    "publishers": [
      {
        "id": 2,
        "book_id": 2,
        "name": "HarperCollins",
        "published_date": "1960-07-11"
      }
    ]
  }
]</span>
</code></pre>
<p>The outcome of this query presents a unified view of books alongside their corresponding publishers.</p>
<h3 id="heading-fine-tuning-queries-with-foreign-table-filters">Fine-Tuning Queries with Foreign Table Filters</h3>
<p>Refine your queries by filtering based on attributes of the foreign table. Here's an illustrative example:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692645704800/5c700d73-95ea-4819-b21a-5addb64da494.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'books'</span>)
    .select(<span class="hljs-string">'*,publishers!inner(name,published_date)'</span>)
    .eq(<span class="hljs-string">'publishers.name'</span>, <span class="hljs-string">'Bloomsbury'</span>);

<span class="hljs-comment">/* Example response:
[
  {
    "id": 6,
    "title": "Harry Potter and the Sorcerer's Stone",
    "author": "J.K. Rowling",
    "publishers": [
      {
        "name": "Bloomsbury",
        "published_date": "1997-06-26"
      }
    ]
  }
]</span>
</code></pre>
<p>This query retrieves books published by "Bloomsbury" along with their corresponding details.</p>
<h3 id="heading-uncover-insights-with-foreign-table-count">Uncover Insights with Foreign Table Count</h3>
<p>You can also use JOINs to retrieve aggregate data from the foreign table. This example showcases counting translations for each book:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692646263183/ac43ff37-cd79-4caf-ae01-a9970d938777.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript">
<span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
  .from(<span class="hljs-string">'books'</span>)
  .select(<span class="hljs-string">`*, translations(count)`</span>)
<span class="hljs-comment">/* Example response:
[
  {
    "id": 1,
    "title": "The Great Gatsby",
    "author": "F. Scott Fitzgerald",
    "translations": [
      {
        "count": 2
      }
    ]
  },
  {
    "id": 2,
    "title": "To Kill a Mockingbird",
    "author": "Harper Lee",
    "translations": [
      {
        "count": 2
      }
    ]
  },
  {
    "id": 3,
    "title": "1984",
    "author": "George Orwell",
    "translations": [
      {
        "count": 2
      }
    ]
  }
]</span>
</code></pre>
<p>This query returns the books along with the count of translations for each.</p>
<h3 id="heading-anti-joins-by-checking-for-nulls">Anti-Joins by checking for nulls</h3>
<p>It is possible to isolate the rows that are shared with the following approach:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692646155506/836a53fb-8b2a-4d92-a3ef-0f997aed3fb5.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-sql">supabase
  .from("books")
  .select("*,translations()")
  .is("translations", null)
</code></pre>
<h3 id="heading-forging-relationships-between-unrelated-tables">Forging Relationships between Unrelated Tables</h3>
<p>Discover a surprising capability: establishing connections between seemingly unrelated tables via the parent table:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1692645783952/f3e8cc43-c176-4425-b02f-bf42c6fc53c3.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'books'</span>).select(<span class="hljs-string">'publishers(*),translations(*)'</span>);
<span class="hljs-comment">/* Example response:
[
  {
    "publishers": [
      {
        "id": 1,
        "book_id": 1,
        "name": "Scribner",
        "published_date": "1925-04-10"
      }
    ],
    "translations": [
      {
        "id": 1,
        "book_id": 1,
        "language": "French",
        "translator": "Maurice Ravel"
      },
      {
        "id": 2,
        "book_id": 1,
        "language": "German",
        "translator": "Franz Kafka"
      }
    ]
  },
  {
    "publishers": [
      {
        "id": 2,
        "book_id": 2,
        "name": "HarperCollins",
        "published_date": "1960-07-11"
      }
    ],
    "translations": [
      {
        "id": 3,
        "book_id": 2,
        "language": "Spanish",
        "translator": "Gabriel García Márquez"
      },
      {
        "id": 4,
        "book_id": 2,
        "language": "Italian",
        "translator": "Italo Calvino"
      }
    ]
  }
]</span>
</code></pre>
<p>In this instance, we amalgamate data from the <code>publishers</code> and <code>translations</code> tables based on the intermediary <code>books</code> table, even without a direct link.</p>
<h2 id="heading-exploring-the-realm-of-computed-relationships">Exploring the Realm of Computed Relationships</h2>
<p>Having mastered foreign keys and JOIN queries, let's elevate our understanding by venturing into computed relationships—a potent feature bestowed by Supabase and PostgreSQL. Computed relationships empower us to bridge gaps between tables and derive meaningful connections among data points.</p>
<h3 id="heading-setting-the-stage-with-example-tables">Setting the Stage with Example Tables</h3>
<p>To illustrate computed relationships, we'll create example tables <code>planets</code> and <code>moons</code>, showcasing how this advanced technique can enhance our data management.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Computed relationships:</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> planets (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">ALWAYS</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> moons (
    <span class="hljs-keyword">id</span> <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">GENERATED</span> <span class="hljs-keyword">ALWAYS</span> <span class="hljs-keyword">AS</span> <span class="hljs-keyword">IDENTITY</span> PRIMARY <span class="hljs-keyword">KEY</span>,
    planet_id <span class="hljs-built_in">INTEGER</span>,
    <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-comment">-- Relationship function (many to one):</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">FUNCTION</span> planets(moons) <span class="hljs-keyword">RETURNS</span> SETOF moons <span class="hljs-keyword">ROWS</span> <span class="hljs-number">1</span> <span class="hljs-keyword">AS</span> $$
  <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> moons <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">id</span> = $<span class="hljs-number">1.</span>planet_id
$$ STABLE <span class="hljs-keyword">LANGUAGE</span> <span class="hljs-keyword">SQL</span>;
</code></pre>
<p>Let's add some rows to these tables to test out our computed relationships:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> planets (<span class="hljs-keyword">name</span>) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-string">'Mercury'</span>),
    (<span class="hljs-string">'Venus'</span>),
    (<span class="hljs-string">'Earth'</span>),
    (<span class="hljs-string">'Mars'</span>),
    (<span class="hljs-string">'Jupiter'</span>),
    (<span class="hljs-string">'Saturn'</span>),
    (<span class="hljs-string">'Uranus'</span>),
    (<span class="hljs-string">'Neptune'</span>);
<span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> moons (planet_id, <span class="hljs-keyword">name</span>) <span class="hljs-keyword">VALUES</span>
    (<span class="hljs-number">3</span>, <span class="hljs-string">'Moon'</span>),
    (<span class="hljs-number">4</span>, <span class="hljs-string">'Phobos'</span>),
    (<span class="hljs-number">4</span>, <span class="hljs-string">'Deimos'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Io'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Europa'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Ganymede'</span>),
    (<span class="hljs-number">5</span>, <span class="hljs-string">'Callisto'</span>),
    (<span class="hljs-number">6</span>, <span class="hljs-string">'Titan'</span>),
    (<span class="hljs-number">6</span>, <span class="hljs-string">'Enceladus'</span>),
    (<span class="hljs-number">7</span>, <span class="hljs-string">'Titania'</span>);
</code></pre>
<h3 id="heading-probing-computed-relationships">Probing Computed Relationships</h3>
<p>With the stage set, let's explore computed relationships using Supabase-JS. The following JavaScript snippet exemplifies querying a computed relationship between the <code>moons</code> and <code>planets</code> tables:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'moons'</span>).select(<span class="hljs-string">'*,planets(*)'</span>);
<span class="hljs-comment">/* Example response:
[
  {
    "id": 1,
    "planet_id": 3,
    "name": "Moon",
    "planets": {
      "id": 3,
      "planet_id": 4,
      "name": "Deimos"
    }
  },
  {
    "id": 2,
    "planet_id": 4,
    "name": "Phobos",
    "planets": {
      "id": 4,
      "planet_id": 5,
      "name": "Io"
    }
  }
]
*/</span>
</code></pre>
<p>The resulting response showcases the enhanced connections between moons and their parent planets.</p>
<h3 id="heading-conclusion">Conclusion</h3>
<p>We demonstrated the potential of Supabase-JS and PostgreSQL, propelling data management to new heights. From rudimentary JOIN queries to sophisticated computed relationships, these tools metamorphose serverless application data management. Armed with these techniques, developers wield the power to craft adaptable, efficient, and interlinked systems tailored to diverse data relationship scenarios.</p>
<p>Embark on your journey with Supabase and PostgreSQL, exploring further features, optimization tactics, and real-world application scenarios. The realm of data management awaits—harness its might with Supabase!</p>
<p>Stay tuned for more illuminating insights and enriching content in forthcoming blog posts.</p>
]]></content:encoded></item><item><title><![CDATA[Integrating Password + Email Sign-in for magic-link users with Supabase and PostgreSQL]]></title><description><![CDATA[In the realm of web development, the popularity of backend-as-a-service (BaaS) platforms has soared due to their convenience and time-saving abilities. One such platform that has garnered attention is Supabase, an open-source BaaS alternative. Supaba...]]></description><link>https://blog.mansueli.com/integrating-password-email-sign-in-for-magic-link-users-with-supabase-and-postgresql</link><guid isPermaLink="true">https://blog.mansueli.com/integrating-password-email-sign-in-for-magic-link-users-with-supabase-and-postgresql</guid><category><![CDATA[supabase]]></category><category><![CDATA[authentication]]></category><category><![CDATA[magic links]]></category><category><![CDATA[PostgreSQL]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 01 Aug 2023 13:25:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690657678369/87698244-c324-4f9c-a0ba-b2f8b5a674e6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the realm of web development, the popularity of backend-as-a-service (BaaS) platforms has soared due to their convenience and time-saving abilities. One such platform that has garnered attention is <a target="_blank" href="http://supabase.com/">Supabase</a>, an open-source BaaS alternative. Supabase offers developers an array of features, including authentication, database management, and more. In this comprehensive guide, we will explore how to enable password + email sign-in for users who initially started with magic links in Supabase. This integration empowers users to effortlessly transition from magic links to password-based authentication.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<p>Before diving into this step-by-step tutorial, ensure you have the following prerequisites in place:</p>
<ol>
<li><p><strong>Deno</strong>: Deno is a secure runtime for JavaScript and TypeScript. Visit the official website for instructions on installing Deno.</p>
</li>
<li><p><strong>Supabase Account</strong>: Create a free <a target="_blank" href="http://supabase.com/">Supabase</a> account if you haven't done so already. You'll require your Supabase URL and anonymous key for authentication.</p>
</li>
<li><p><strong>PostgreSQL</strong>: Ensure you have PostgreSQL installed or set up a PostgreSQL database using Supabase.</p>
</li>
</ol>
<p>To maximize your understanding of this tutorial, it's beneficial to have a basic grasp of Supabase, PostgreSQL, and HTTP requests.</p>
<h2 id="heading-setting-up-the-database">Setting Up the Database</h2>
<p>Let's begin by assuming you have one of the web <a target="_blank" href="https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs">app templates</a> with a <code>public.profile</code> table. We'll specifically add a new column to the "profiles" table to indicate whether existing users are using magic links.</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Setting the profiles table to indicate that existing users were using magic links</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> public.profiles 
<span class="hljs-keyword">ADD</span> <span class="hljs-keyword">COLUMN</span> auth_options <span class="hljs-built_in">text</span>[] <span class="hljs-keyword">DEFAULT</span> <span class="hljs-built_in">ARRAY</span>[<span class="hljs-string">'magiclink'</span>]::<span class="hljs-built_in">text</span>[] <span class="hljs-literal">NULL</span>;
</code></pre>
<h2 id="heading-implementing-the-edge-function">Implementing the Edge Function</h2>
<p>The edge function plays a pivotal role in this integration as it verifies the user's authentication method (magic link or password) and updates the necessary information accordingly. Below is the Deno code for the edge function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { serve } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/std@0.192.0/http/server.ts"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"https://esm.sh/@supabase/supabase-js@2"</span>;

<span class="hljs-comment">// CORS headers for allowing requests from any origin</span>
<span class="hljs-keyword">const</span> corsHeaders = {
  <span class="hljs-string">"Access-Control-Allow-Origin"</span>: <span class="hljs-string">"*"</span>,
  <span class="hljs-string">"Access-Control-Allow-Headers"</span>:
    <span class="hljs-string">"authorization, x-client-info, apikey, content-type"</span>,
};

serve(<span class="hljs-keyword">async</span> (req) =&gt; {
  <span class="hljs-keyword">if</span> (req.method === <span class="hljs-string">'OPTIONS'</span>) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>, { headers: corsHeaders });
  }

  <span class="hljs-comment">// Creating a Supabase client with the provided authorization token</span>
  <span class="hljs-keyword">const</span> supabaseClient = createClient(
    Deno.env.get(<span class="hljs-string">"SUPABASE_URL"</span>) ?? <span class="hljs-string">""</span>,
    Deno.env.get(<span class="hljs-string">"SUPABASE_ANON_KEY"</span>) ?? <span class="hljs-string">""</span>,
    {
      <span class="hljs-built_in">global</span>: { headers: { Authorization: req.headers.get(<span class="hljs-string">"Authorization"</span>)! } },
      auth: {
        autoRefreshToken: <span class="hljs-literal">false</span>,
        persistSession: <span class="hljs-literal">false</span>,
        detectSessionInUrl: <span class="hljs-literal">false</span>
      }
    }
  );

  <span class="hljs-comment">// Getting the user information from the provided authorization token</span>
  <span class="hljs-keyword">const</span> { data: { user }, error: userError } = <span class="hljs-keyword">await</span> supabaseClient.auth.getUser();
  <span class="hljs-keyword">if</span> (userError) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"User error"</span>, userError);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: userError.message }), {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">400</span>,
    });
  }

  <span class="hljs-comment">// Getting the current authentication options for the user from the profiles table</span>
  <span class="hljs-keyword">const</span> { data: auth_options, error: authError } = <span class="hljs-keyword">await</span> supabaseClient.from(<span class="hljs-string">'profiles'</span>).select(<span class="hljs-string">'auth_options'</span>).eq(<span class="hljs-string">'id'</span>, user.id);
  <span class="hljs-keyword">if</span> (authError) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Auth options error"</span>, authError);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: authError.message }), {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">400</span>,
    });
  }

  <span class="hljs-comment">// Checking if the user already has password-based authentication</span>
  <span class="hljs-keyword">if</span> (auth_options[<span class="hljs-number">0</span>].auth_options.includes(<span class="hljs-string">"password"</span>)) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Password update not allowed for users with password-based authentication"</span>);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: <span class="hljs-string">"Password update not allowed for users with password-based authentication"</span> }), {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">400</span>,
    });
  }

  <span class="hljs-comment">// Creating a separate Supabase client with the service role key for administrative tasks</span>
  <span class="hljs-keyword">const</span> supabaseAdmin = createClient(
    Deno.env.get(<span class="hljs-string">"SUPABASE_URL"</span>) ?? <span class="hljs-string">""</span>,
    Deno.env.get(<span class="hljs-string">"SUPABASE_SERVICE_ROLE_KEY"</span>) ?? <span class="hljs-string">""</span>,
    {
      auth: {
        autoRefreshToken: <span class="hljs-literal">false</span>,
        persistSession: <span class="hljs-literal">false</span>,
        detectSessionInUrl: <span class="hljs-literal">false</span>
      }
    }
  );

  <span class="hljs-comment">// Updating the user's password using the administrative client</span>
  <span class="hljs-keyword">const</span> { newPassword } = <span class="hljs-keyword">await</span> req.json();
  <span class="hljs-keyword">const</span> { error: updateError } = <span class="hljs-keyword">await</span> supabaseAdmin.auth.admin.updateUserById(user.id, { password: newPassword });
  <span class="hljs-keyword">if</span> (updateError) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Update error"</span>, updateError);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: updateError.message }), {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">400</span>,
    });
  }

  <span class="hljs-comment">// Updating the authentication options for the user in the profiles table</span>
  <span class="hljs-keyword">const</span> updatedAuthOptions = [...auth_options[<span class="hljs-number">0</span>].auth_options];
  <span class="hljs-keyword">if</span> (!updatedAuthOptions.includes(<span class="hljs-string">"password"</span>)) {
    updatedAuthOptions.push(<span class="hljs-string">"password"</span>);
  }

  <span class="hljs-keyword">const</span> { error: updateAuthOptionsError } = <span class="hljs-keyword">await</span> supabaseClient.from(<span class="hljs-string">'profiles'</span>).update({ auth_options: updatedAuthOptions });
  <span class="hljs-keyword">if</span> (updateAuthOptionsError) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Update auth options error"</span>, updateAuthOptionsError);
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-built_in">JSON</span>.stringify({ error: updateAuthOptionsError.message }), {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">400</span>,
    });
  }

  <span class="hljs-comment">// Returning a success message if everything went smoothly</span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(
    <span class="hljs-built_in">JSON</span>.stringify({ message: <span class="hljs-string">"Password updated successfully"</span> }),
    {
      headers: { ...corsHeaders, <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span> },
      status: <span class="hljs-number">200</span>,
    },
  );
});
</code></pre>
<h2 id="heading-testing-the-integration">Testing the Integration</h2>
<p>To ensure the seamless functioning of our integration, let's conduct some tests encompassing various scenarios:</p>
<ol>
<li><p>Test with a user who has been using magic links and now wants to set a password.</p>
</li>
<li><p>Test with a user who already has a password set to ensure password updates are not allowed for such users.</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Congratulations on successfully integrating password + email sign-in with Supabase and PostgreSQL! This integration provides users with the flexibility to switch from magic links to password-based authentication effortlessly, enhancing the overall user experience. By harnessing the power of Supabase's open-source backend-as-a-service capabilities and PostgreSQL's robustness, you can build secure and user-friendly applications more efficiently.</p>
]]></content:encoded></item><item><title><![CDATA[Safeguarding Data Integrity with pg-safeupdate in PostgreSQL and Supabase]]></title><description><![CDATA[Maintaining data integrity is of paramount importance when developing web applications that rely on PostgreSQL as the underlying database. Accidental data modifications can lead to severe consequences, compromising the reliability and accuracy of the...]]></description><link>https://blog.mansueli.com/safeguarding-data-integrity-with-pg-safeupdate-in-postgresql-and-supabase</link><guid isPermaLink="true">https://blog.mansueli.com/safeguarding-data-integrity-with-pg-safeupdate-in-postgresql-and-supabase</guid><category><![CDATA[supabase]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[SQL]]></category><category><![CDATA[Data Integrity]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 25 Jul 2023 12:30:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1690287674112/963a9390-a4f3-4b28-bb3b-9009e04a10c7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Maintaining data integrity is of paramount importance when developing web applications that rely on PostgreSQL as the underlying database. Accidental data modifications can lead to severe consequences, compromising the reliability and accuracy of the application. In this blog post, we will explore how PostgreSQL's <code>pg-safeupdate</code> extension, in conjunction with <a target="_blank" href="http://supabase.com/">Supabase.com</a>, a versatile backend-as-a-service platform, can be a powerful tool to ensure data safety and integrity.</p>
<h3 id="heading-what-is-pg-safeupdate"><strong>What is pg-safeupdate?</strong></h3>
<p><code>pg-safeupdate</code> plays a crucial role in PostgreSQL, aiming to mitigate the risks linked with unintended data updates. Its primary goal is to prevent update queries lacking a WHERE clause, which can inadvertently update all rows in a table. Enforcing the presence of a WHERE clause, <code>pg-safeupdate</code> ensures explicit and targeted updates, significantly reducing the risk of accidental data alterations.</p>
<h3 id="heading-enabling-the-extension"><strong>Enabling the Extension</strong></h3>
<p>Enabling <code>pg-safeupdate</code> can be done at two levels: on a per-connection basis or for all connections to the database. To enable it for a specific connection, you can use the following SQL command:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">LOAD</span> <span class="hljs-string">'safeupdate'</span>;
</code></pre>
<p>For broader applications, you can enable <code>pg-safeupdate</code> for all connections to the database with the following command:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">DATABASE</span> my_app_db <span class="hljs-keyword">SET</span> session_preload_libraries = <span class="hljs-string">'safeupdate'</span>;
</code></pre>
<p>The choice between the two methods depends on the specific use case and the desired level of data protection.</p>
<h3 id="heading-how-pg-safe-update-works"><strong>How pg-safe update Works:</strong></h3>
<p>Once <code>pg-safeupdate</code> is enabled, it actively monitors all update queries issued against the database. If an update query is detected without a WHERE clause, indicating an attempt to update all rows in the affected table, <code>pg-safeupdate</code> it intervenes and returns an error message. This prompt reminds developers to include a WHERE clause, ensuring that updates are precise, deliberate, and confined to specific rows.</p>
<p><strong>Usage Example:</strong> To better understand the functioning of <code>pg-safeupdate</code>, let's consider a real-world example involving a <code>inventory</code> table. This table contains columns such as <code>product_id</code>, <code>product_name</code>, <code>quantity</code>, and <code>price_per_unit</code>. Without <code>pg-safeupdate</code>, an unintentional update query might look like this:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">LOAD</span> <span class="hljs-string">'safeupdate'</span>;

<span class="hljs-keyword">UPDATE</span> inventory <span class="hljs-keyword">SET</span> quantity = <span class="hljs-number">100</span>;
</code></pre>
<p>This query updates the <code>quantity</code> for all products to 100, which is certainly not what we intended. However, with <code>pg-safeupdate</code> enabled, the query will be intercepted, and an error message will be returned:</p>
<pre><code class="lang-sql">ERROR: <span class="hljs-keyword">UPDATE</span> requires a <span class="hljs-keyword">WHERE</span> clause.
</code></pre>
<p>To proceed with the update, we must provide a WHERE clause that specifies the <code>product_id</code> of the product we want to modify:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">UPDATE</span> inventory <span class="hljs-keyword">SET</span> quantity = <span class="hljs-number">100</span> <span class="hljs-keyword">WHERE</span> product_id = <span class="hljs-string">'ABC123'</span>;
</code></pre>
<p>This updated query now precisely modifies the quantity of the product with the <code>product_id</code> of 'ABC123', ensuring data integrity.</p>
<p><strong>Real-world Use Cases:</strong> In real-world scenarios, <code>pg-safeupdate</code> becomes a powerful safeguard against accidental data alterations. It prevents catastrophic data corruption in production environments, where unintended updates could lead to substantial financial losses or erode user trust. Additionally, during development and testing, <code>pg-safeupdate</code> ensures that database changes are deliberate, minimizing debugging efforts caused by unintentional data modifications.</p>
<p><strong>Comparisons with Alternative Methods:</strong> While several methods exist for ensuring data integrity, <code>pg-safeupdate</code> stands out for its simplicity and effectiveness. Database triggers can achieve similar outcomes, but they might require more complex configurations and can be less transparent. Application-level constraints depend on the application code's correctness, which introduces a higher risk of human error. In contrast, <code>pg-safeupdate</code> offers a direct and efficient approach to enforcing update restrictions.</p>
<p><strong>Conclusion:</strong> In the world of web applications, data integrity is non-negotiable. PostgreSQL's <code>pg-safeupdate</code> extension, in collaboration with Supabase, presents a formidable solution to mitigate the risks of accidental data updates. Mandating the use of WHERE clauses in update queries <code>pg-safeupdate</code> ensures that updates are intentional and specific. When combined with Supabase's features, developers can create robust and reliable web applications, safeguarding data from inadvertent modifications.</p>
<p><strong>Additional Tips and Best Practices:</strong></p>
<ul>
<li><p>Enable <code>pg-safeupdate</code> during development and testing phases to catch unintended updates early in the development process.</p>
</li>
<li><p>Always include a WHERE clause in update queries, even when the extension is not enabled, as a best practice for data safety.</p>
</li>
<li><p>Consider enabling <code>pg-safeupdate</code> for all connections in production environments to enforce strict update restrictions consistently.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Leveraging Supabase and PostgreSQL for Distance-based Filtering and Location Data Retrieval]]></title><description><![CDATA[In this blog post, we will explore how to leverage Supabase as a backend and PostgreSQL as the underlying database to implement distance-based filtering and retrieve location data. We will also highlight the significance of incorporating geospatial f...]]></description><link>https://blog.mansueli.com/leveraging-supabase-and-postgresql-for-distance-based-filtering-and-location-data-retrieval</link><guid isPermaLink="true">https://blog.mansueli.com/leveraging-supabase-and-postgresql-for-distance-based-filtering-and-location-data-retrieval</guid><category><![CDATA[PostgreSQL]]></category><category><![CDATA[supabase]]></category><category><![CDATA[location api]]></category><category><![CDATA[Geospatial]]></category><category><![CDATA[Databases]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 18 Jul 2023 14:00:09 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1689109315741/000a27ab-c114-4f52-ba66-45e888b4f3d5.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In this blog post, we will explore how to leverage <a target="_blank" href="http://supabase.com/">Supabase</a> as a backend and PostgreSQL as the underlying database to implement distance-based filtering and retrieve location data. We will also highlight the significance of incorporating geospatial functionalities using <a target="_blank" href="https://postgis.net/">PostGIS</a>.</p>
<h3 id="heading-geospatial-functionalities-with-postgis">Geospatial Functionalities with PostGIS</h3>
<p>When working with geospatial data in PostgreSQL, integrating <a target="_blank" href="https://postgis.net/">PostGIS</a> becomes essential. PostGIS is an open-source extension to PostgreSQL that enables the storage, management, and analysis of geographic data. It provides a wide range of geospatial functions and operators that can be utilized to perform complex spatial queries and manipulations. By incorporating PostGIS into your Supabase and PostgreSQL stack, you can leverage powerful capabilities such as distance calculations, spatial indexing, geometric operations, and spatial relationships. These functionalities enable efficient distance-based filtering and location data retrieval, making it easier to implement location-aware applications and services.</p>
<h3 id="heading-retrieving-location-data">Retrieving Location Data</h3>
<p>To obtain location data based on an IP address, we can create two PostgreSQL functions: <code>get_records_within_distance_free</code> for the free tier (60 requests per minute) and <code>get_records_within_distance</code> when you have an API key from the <a target="_blank" href="https://freeipapi.com/">freeipapi</a> service.</p>
<p>a. <code>get_records_within_distance_free</code> Function (Free Tier):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> get_records_within_distance_free(ip <span class="hljs-built_in">varchar</span>, distance <span class="hljs-built_in">numeric</span>)
<span class="hljs-keyword">RETURNS</span> SETOF my_table <span class="hljs-keyword">AS</span>
$$
<span class="hljs-keyword">DECLARE</span>
    tlongitude <span class="hljs-built_in">float</span>;
    tlatitude float;
    response jsonb;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- HTTP GET request to retrieve the latitude and longitude</span>
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">content</span>::jsonb <span class="hljs-keyword">INTO</span> response <span class="hljs-keyword">FROM</span> http_get(<span class="hljs-string">'https://freeipapi.com/api/json/'</span> || ip);
    tlongitude := (response -&gt; 'longitude')::float;
    tlatitude := (response -&gt; 'latitude')::float;
    <span class="hljs-comment">-- Call the get_entries_within_distance function</span>
    RETURN QUERY <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> get_entries_within_distance(tlongitude, tlatitude, distance);
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql VOLATILE;
</code></pre>
<p>The <code>get_records_within_distance_free</code> function retrieves the latitude and longitude by making an HTTP GET request to the <a target="_blank" href="https://freeipapi.com/">freeipapi</a> API. It then calls the <code>get_entries_within_distance</code> function using the obtained latitude, longitude, and distance parameters.</p>
<p>b. <code>get_records_within_distance</code> Function (API Key):</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> get_records_within_distance(ip <span class="hljs-built_in">varchar</span>, distance <span class="hljs-built_in">numeric</span>)
<span class="hljs-keyword">RETURNS</span> SETOF my_table <span class="hljs-keyword">AS</span>
$$
<span class="hljs-keyword">DECLARE</span>
    tlongitude <span class="hljs-built_in">float</span>;
    tlatitude float;
    response jsonb;
    freeipapi_key TEXT;
<span class="hljs-keyword">BEGIN</span>
    <span class="hljs-comment">-- Get freeipapi_key from the vault</span>
    <span class="hljs-keyword">SELECT</span> decrypted_secret
    <span class="hljs-keyword">INTO</span> freeipapi_key
    <span class="hljs-keyword">FROM</span> vault.decrypted_secrets
    <span class="hljs-keyword">WHERE</span> <span class="hljs-keyword">name</span> = <span class="hljs-string">'freeipapi_key'</span>;
    <span class="hljs-comment">-- HTTP GET request to retrieve the latitude and longitude</span>
    <span class="hljs-keyword">SELECT</span> <span class="hljs-keyword">content</span>::jsonb <span class="hljs-keyword">INTO</span> response
    <span class="hljs-keyword">FROM</span> <span class="hljs-keyword">http</span>((
        <span class="hljs-string">'GET'</span>,
        <span class="hljs-string">'https://freeipapi.com/api/json/'</span> || ip,
        <span class="hljs-built_in">ARRAY</span>[http_header(<span class="hljs-string">'Authorization'</span>, <span class="hljs-string">'Bearer '</span> || freeipapi_key)],
        <span class="hljs-string">'application/json'</span>
    )::http_request);
    tlongitude := (response -&gt; 'longitude')::float;
    tlatitude := (response -&gt; 'latitude')::float;
    <span class="hljs-comment">-- Call the get_entries_within_distance function</span>
    RETURN QUERY <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> get_entries_within_distance(tlongitude, tlatitude, distance);
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql VOLATILE;
</code></pre>
<p>The <code>get_records_within_distance</code> function is used when you have an API key from the <a target="_blank" href="https://freeipapi.com/">freeipapi</a> service. It retrieves the API key from the vault and incorporates it into the HTTP GET request to retrieve the latitude and longitude. The obtained latitude, longitude, and distance parameters are then used to call the <code>get_entries_within_distance</code> function.</p>
<h3 id="heading-leveraging-postgresql-function-for-distance-based-filtering">Leveraging PostgreSQL Function for Distance-Based Filtering</h3>
<p>In this section, we will delve into the creation of a PostgreSQL function to enable distance-based filtering, leveraging the power of the PostGIS extension. This approach allows efficient retrieval of location data within a specified radius. Implementing this functionality with Supabase and PostgreSQL empowers your application with geospatial capabilities.</p>
<p>Let's explore an example of the PostgreSQL function, <code>get_entries_within_distance</code>, designed to facilitate distance-based filtering:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> get_entries_within_distance(tlongitude <span class="hljs-built_in">float</span>, tlatitude <span class="hljs-built_in">float</span>, distance <span class="hljs-built_in">numeric</span>)
<span class="hljs-keyword">RETURNS</span> SETOF my_table <span class="hljs-keyword">AS</span>
$$
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">RETURN</span> <span class="hljs-keyword">QUERY</span> <span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> my_table 
  <span class="hljs-keyword">WHERE</span> ST_DistanceSphere(ST_MakePoint(longitude, latitude), ST_MakePoint(tlongitude, tlatitude)) &lt; distance::<span class="hljs-built_in">float</span>;
<span class="hljs-keyword">END</span>;
$$ LANGUAGE plpgsql VOLATILE;
</code></pre>
<h3 id="heading-executing-the-postgresql-function">Executing the PostgreSQL Function</h3>
<p>To retrieve records within a specific distance from a given IP address, we can execute the appropriate PostgreSQL function using SQL queries. Here are two examples:</p>
<p>a. Utilizing the Free Tier Function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> get_records_within_distance_free(<span class="hljs-string">'138.186.111.34'</span>, <span class="hljs-number">900000</span>);
</code></pre>
<p>b. Employing the API Key Function:</p>
<pre><code class="lang-sql"><span class="hljs-keyword">SELECT</span> * <span class="hljs-keyword">FROM</span> get_records_within_distance(<span class="hljs-string">'138.186.111.34'</span>, <span class="hljs-number">900000</span>);
</code></pre>
<p>Both queries retrieve records that fall within a distance of 900,000 units from the geographical location associated with the IP address '138.186.111.36'.</p>
<h3 id="heading-invoking-the-postgresql-function-from-a-javascript-client">Invoking the PostgreSQL Function from a JavaScript Client</h3>
<p>To integrate the PostgreSQL function into your JavaScript application using Supabase's JavaScript client library, you can utilize RPC (Remote Procedure Call) to make the necessary call. Here's an optimized code snippet:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: ret, error } = <span class="hljs-keyword">await</span> supabase
  .rpc(<span class="hljs-string">'get_records_within_distance'</span>, { <span class="hljs-attr">ip</span>: <span class="hljs-string">'138.186.111.34'</span>, <span class="hljs-attr">distance</span>: <span class="hljs-number">900000</span> });
<span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify(ret));
</code></pre>
<p>In this example, the IP address and distance parameters are passed to the <code>get_records_within_distance</code> function, and the response is stored in the <code>ret</code> variable.</p>
<h3 id="heading-security-considerations-for-accessing-the-freeipapicom-api">Security Considerations for Accessing the freeipapi.com API</h3>
<p>When it comes to accessing external APIs like freeipapi.com, it's crucial to prioritize security and adhere to rate limits. By utilizing the <code>get_records_within_distance</code> function, which retrieves the <code>freeipapi_key</code> from the vault, you can effectively bypass the rate limit imposed by the API, which typically allows only 60 requests per minute. However, it's essential to implement rate limit handling tailored to your specific application requirements, ensuring a seamless and efficient user experience.</p>
<h3 id="heading-conclusion-leveraging-supabase-postgresql-and-geospatial-capabilities-for-location-data-retrieval">Conclusion: Leveraging Supabase, PostgreSQL, and Geospatial Capabilities for Location Data Retrieval</h3>
<p>In this blog post, we explored the powerful combination of Supabase as a backend and PostgreSQL as the underlying database to implement distance-based filtering and retrieve location data. By incorporating the robust geospatial functionalities provided by PostGIS and leveraging the freeipapi.com API, we were able to achieve accurate distance calculations and obtain precise location information.</p>
<p>The integration of Supabase, PostgreSQL, and external APIs empowers developers to build highly sophisticated and location-aware applications. Whether you're working on mapping services, location-based search functionality, or geospatial analytics, this comprehensive stack equips you with the necessary tools and flexibility to deliver exceptional user experiences.</p>
<p>We highly encourage readers to continue exploring and experimenting with Supabase, PostgreSQL, PostGIS, and other cutting-edge geospatial technologies to unlock the full potential of location data in their projects. By leveraging these state-of-the-art technologies, you can create innovative applications that seamlessly integrate location awareness and provide valuable insights to your users. Including expanding on the <a target="_blank" href="https://blog.mansueli.com/tracking-user-data-with-fingerprint-and-supabase-in-postgresql">user tracking</a> mechanisms.</p>
<p>Stay up-to-date with the official documentation of Supabase, PostgreSQL, and PostGIS to leverage the latest features, enhancements, and security best practices. As you embark on your development journey, we wish you success in creating groundbreaking location-enabled applications that make a significant impact in your industry.</p>
]]></content:encoded></item><item><title><![CDATA[Testing Supabase Edge Functions with Deno Test]]></title><description><![CDATA[Backend development requires robust and reliable tools in today's rapidly evolving technology landscape. While Firebase has been a go-to choice for many developers, there is now an open-source alternative called Supabase that offers a PostgreSQL-base...]]></description><link>https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test</link><guid isPermaLink="true">https://blog.mansueli.com/testing-supabase-edge-functions-with-deno-test</guid><category><![CDATA[Deno]]></category><category><![CDATA[supabase]]></category><category><![CDATA[Edge-Functions]]></category><category><![CDATA[unit testing]]></category><dc:creator><![CDATA[Rodrigo Mansueli]]></dc:creator><pubDate>Tue, 11 Jul 2023 13:08:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1688568563653/68f370fa-df30-4f9e-a68e-7363662c1a64.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Backend development requires robust and reliable tools in today's rapidly evolving technology landscape. While Firebase has been a go-to choice for many developers, there is now an open-source alternative called Supabase that offers a PostgreSQL-based backend. <a target="_blank" href="https://supabase.com/">Supabase</a> combines the robustness of PostgreSQL with the ease of use and scalability of Firebase, providing a compelling solution for backend development.</p>
<p>One crucial aspect of utilizing Supabase is testing its Edge Functions, which play a vital role in extending the functionality of your application while ensuring its reliability. In this blog post, we will explore the process of setting up Supabase, delve into the world of Supabase Edge Functions, and learn how to write and test a simple <code>hello-world</code> function and run Edge Functions locally for efficient development. I previously wrote about <a target="_blank" href="https://blog.mansueli.com/using-supabase-js-as-a-script-in-your-terminal">testing Supabase-JS in your terminal</a> which can also help to build a foundation.</p>
<h3 id="heading-setting-up-supabase-and-postgresql">Setting up Supabase and PostgreSQL</h3>
<p>To get started with Supabase, we must first install and set it up using the Command Line Interface (CLI). The CLI offers a convenient way to initialize and manage Supabase projects effortlessly. Let's walk through the steps to install Supabase CLI and set up a Supabase project:</p>
<ol>
<li><p>Install Docker: Supabase relies on Docker for local development. Install Docker by following the instructions on the official <a target="_blank" href="https://docs.docker.com/desktop/install/mac-install/">Docker website</a>.</p>
</li>
<li><p>Install Supabase CLI: Open your terminal and run the following command to install the Supabase CLI:</p>
<pre><code class="lang-bash"> npm install -g supabase
</code></pre>
</li>
<li><p>Initialize a Supabase project: In your terminal, navigate to the desired directory and run the following command to initialize a new Supabase project:</p>
<pre><code class="lang-bash"> supabase init
</code></pre>
<p> This command sets up a new Supabase project and creates the necessary configuration files.</p>
</li>
<li><p>Start the Supabase server: Once the initialization is complete, start the Supabase server using the following command:</p>
<pre><code class="lang-bash"> supabase start
</code></pre>
<p> This command starts the Supabase server locally on your machine, allowing you to interact with the Supabase backend.</p>
</li>
<li><p>PostgreSQL as the underlying database: Supabase leverages PostgreSQL as its underlying database. This powerful and reliable database management system ensures the stability and scalability of your backend. You can interact with PostgreSQL using Supabase's intuitive APIs and tools.</p>
</li>
</ol>
<h3 id="heading-overview-of-supabase-edge-functions">Overview of Supabase Edge Functions</h3>
<p>Supabase Edge Functions are an integral part of the Supabase ecosystem, empowering developers to extend the functionality of their applications with serverless computing. These functions run at the edge of the Supabase infrastructure, ensuring optimal performance and reduced latency. Let's explore the advantages of using Supabase Edge Functions:</p>
<ul>
<li><p>Serverless computing: Supabase Edge Functions provide a serverless architecture, allowing you to focus on writing code without the need to manage server infrastructure. This results in reduced operational overhead and improved scalability.</p>
</li>
<li><p>TypeScript support: Edge Functions can be written in TypeScript, a statically-typed superset of JavaScript. TypeScript brings type safety to your code, enabling early detection of potential errors and improving overall code quality.</p>
</li>
<li><p>Seamless integration with Supabase client: Edge Functions seamlessly integrates with the Supabase client, enabling easy communication with your Supabase backend. This integration allows you to leverage the full power of Supabase's APIs and functionalities within your Edge Functions.</p>
</li>
</ul>
<h3 id="heading-understanding-the-hello-world-edge-function-with-cors">Understanding the <code>hello-world</code> Edge Function with CORS</h3>
<p>Now, let's dive into the details of the "Hello-world" Edge Function written in TypeScript and explore its functionality along with Cross-Origin Resource Sharing (CORS) implementation. We'll break down the code step by step, providing a clear understanding of each section and its interaction with the Supabase client.</p>
<p>Here's the code snippet for the <code>hello-world</code> Edge Function:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// Import the serve function from the Deno standard library</span>
<span class="hljs-keyword">import</span> {
    serve
} <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/std@0.192.0/http/server.ts"</span>

<span class="hljs-comment">// Print a greeting message to the console</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Hello from Functions!"</span>)

<span class="hljs-comment">// Define the headers to handle CORS (Cross-Origin Resource Sharing)</span>
<span class="hljs-keyword">const</span> corsHeaders = {
    <span class="hljs-string">"Access-Control-Allow-Origin"</span>: <span class="hljs-string">"*"</span>,
    <span class="hljs-string">"Access-Control-Allow-Headers"</span>: <span class="hljs-string">"authorization, x-client-info, apikey, content-type"</span>,
};

<span class="hljs-comment">// Start the server and define the request handler</span>
serve(<span class="hljs-keyword">async</span>(req) =&gt; {
    <span class="hljs-comment">// If the HTTP method is OPTIONS, return a response with CORS headers</span>
    <span class="hljs-keyword">if</span> (req.method === <span class="hljs-string">'OPTIONS'</span>) {
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(<span class="hljs-string">'ok'</span>, {
            <span class="hljs-attr">headers</span>: corsHeaders
        })
    }
    <span class="hljs-comment">// Extract the name from the request body</span>
    <span class="hljs-keyword">const</span> {
        name
    } = <span class="hljs-keyword">await</span> req.json()
    <span class="hljs-comment">// Prepare the response data</span>
    <span class="hljs-keyword">const</span> data = {
        <span class="hljs-attr">message</span>: <span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>!`</span>,
    }

    <span class="hljs-comment">// Return a response with the data and headers</span>
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Response(
        <span class="hljs-built_in">JSON</span>.stringify(data), {
            <span class="hljs-attr">headers</span>: {
                ...corsHeaders,
                <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>
            },
        }
    )
})
</code></pre>
<p>In this section, we'll analyze the different aspects and functionalities of the <code>Hello-world</code> Edge Function, providing insights into each segment's purpose and its seamless integration with the Supabase client.</p>
<ul>
<li><p>We import the <code>serve</code> function from the Deno standard library to create a server that listens for incoming HTTP requests.</p>
</li>
<li><p>The <code>console.log</code> statement is for logging purposes and will be displayed in the console when the function is executed.</p>
</li>
<li><p>We define the <code>corsHeaders</code> object to handle Cross-Origin Resource Sharing (CORS) headers, allowing requests from any origin and specifying the allowed headers.</p>
</li>
<li><p>The <code>serve</code> function takes an asynchronous callback function as a parameter, which handles the incoming requests.</p>
</li>
<li><p>In the callback function, we check if the request method is <code>OPTIONS</code>. If it is, we return a response with a status of 'ok' and the CORS headers.</p>
</li>
<li><p>If the request method is not <code>OPTIONS</code>, we parse the JSON data from the request body and extract the <code>name</code> field.</p>
</li>
<li><p>We construct a response object with a JSON payload containing a dynamic greeting message using the provided name.</p>
</li>
<li><p>Finally, we return the response object along with the CORS headers.</p>
</li>
</ul>
<p>Now, we need to <a target="_blank" href="https://supabase.com/docs/guides/functions/local-development#running-edge-functions-locally">run the function locally</a> :</p>
<pre><code class="lang-bash">supabase <span class="hljs-built_in">functions</span> serve
</code></pre>
<h3 id="heading-testing-supabase-edge-functions">Testing Supabase Edge Functions</h3>
<p>Testing is a crucial step in the development process to ensure the correctness and performance of your Edge Functions. Let's discuss the importance of testing and examining the code in the <code>deno-test.ts</code> file, which demonstrates how to test the Supabase client and the <code>hello-world</code> function.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// deno-test.ts</span>
<span class="hljs-comment">// This imported is needed to load the .env file:</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"https://deno.land/x/dotenv/load.ts"</span>;
<span class="hljs-comment">// Import necessary libraries and modules</span>
<span class="hljs-keyword">import</span> {
  assert,
  assertExists,
  assertEquals,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"https://deno.land/std@0.192.0/testing/asserts.ts"</span>;
<span class="hljs-keyword">import</span> {
  createClient,
  SupabaseClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"https://esm.sh/@supabase/supabase-js@2.23.0"</span>;
<span class="hljs-keyword">import</span> { delay } <span class="hljs-keyword">from</span> <span class="hljs-string">'https://deno.land/x/delay@v0.2.0/mod.ts'</span>;

<span class="hljs-comment">// Setup the Supabase client configuration</span>
<span class="hljs-keyword">const</span> supabaseUrl = Deno.env.get(<span class="hljs-string">"SUPABASE_URL"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> supabaseKey = Deno.env.get(<span class="hljs-string">"SUPABASE_ANON_KEY"</span>) ?? <span class="hljs-string">""</span>;
<span class="hljs-keyword">const</span> options = {
  auth: {
    autoRefreshToken: <span class="hljs-literal">false</span>,
    persistSession: <span class="hljs-literal">false</span>,
    detectSessionInUrl: <span class="hljs-literal">false</span>
  }
};

<span class="hljs-comment">// Test the creation and functionality of the Supabase client</span>
<span class="hljs-keyword">const</span> testClientCreation = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">var</span> client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options);

  <span class="hljs-comment">// Check if the Supabase URL and key are provided</span>
  <span class="hljs-keyword">if</span> (!supabaseUrl) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'supabaseUrl is required.'</span>)
  <span class="hljs-keyword">if</span> (!supabaseKey) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'supabaseKey is required.'</span>)

  <span class="hljs-comment">// Test a simple query to the database</span>
  <span class="hljs-keyword">const</span> { data: table_data, error: table_error } = <span class="hljs-keyword">await</span> client.from(<span class="hljs-string">'my_table'</span>).select(<span class="hljs-string">'*'</span>).limit(<span class="hljs-number">1</span>);
  <span class="hljs-keyword">if</span> (table_error) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid Supabase client: '</span> + table_error.message);
  }
  assert(table_data, <span class="hljs-string">"Data should be returned from the query."</span>);
};

<span class="hljs-comment">// Test the 'hello-world' function</span>
<span class="hljs-keyword">const</span> testHelloWorld = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">var</span> client: SupabaseClient = createClient(supabaseUrl, supabaseKey, options);

  <span class="hljs-comment">// Invoke the 'hello-world' function with a parameter</span>
  <span class="hljs-keyword">const</span> { data: func_data, error: func_error } = <span class="hljs-keyword">await</span> client.functions.invoke(<span class="hljs-string">'hello-world'</span>, {
    body: { name: <span class="hljs-string">'bar'</span> }
  });

  <span class="hljs-comment">// Check for errors from the function invocation</span>
  <span class="hljs-keyword">if</span> (func_error) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid response: '</span> + func_error.message);
  }

  <span class="hljs-comment">// Log the response from the function</span>
  <span class="hljs-built_in">console</span>.log(<span class="hljs-built_in">JSON</span>.stringify(func_data, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));

  <span class="hljs-comment">// Assert that the function returned the expected result</span>
  assertEquals(func_data.message, <span class="hljs-string">'Hello bar!'</span>);
};

<span class="hljs-comment">// Register and run the tests</span>
Deno.test(<span class="hljs-string">"Client Creation Test"</span>, testClientCreation);
Deno.test(<span class="hljs-string">"Hello-world Function Test"</span>, testHelloWorld);
</code></pre>
<p>This is a test case with two parts. The first tests the client library and check that the database is connectable and returning values from a table (<code>my_table</code>). The second part is testing the edge function and checks if the value received is expected. Here's a brief overview of the code:</p>
<ul>
<li><p>We import various testing functions from the Deno standard library, including <code>assert</code>, <code>assertExists</code>, and <code>assertEquals</code>.</p>
</li>
<li><p>We import the <code>createClient</code> and <code>SupabaseClient</code> classes from the <code>@supabase/supabase-js</code> library to interact with the Supabase client.</p>
</li>
<li><p>We define the necessary configuration for the Supabase client, including the Supabase URL, API key, and authentication options.</p>
</li>
<li><p>The <code>testClientCreation</code> function tests the creation of a Supabase client instance and queries the database for data from a table. It asserts that data is returned from the query.</p>
</li>
<li><p>The <code>testHelloWorld</code> function tests the "Hello-world" Edge Function by invoking it using the Supabase client's <code>functions.invoke</code> method. It checks if the response message matches the expected greeting.</p>
</li>
<li><p>We run the tests using the <code>Deno.test</code> function, providing a descriptive name for each test case and the corresponding test function.</p>
</li>
</ul>
<p>Note: Please make sure to replace the placeholders (<code>supabaseUrl</code>, <code>supabaseKey</code>, <code>my_table</code>) with the actual values relevant to your Supabase setup.</p>
<h3 id="heading-running-edge-functions-locally">Running Edge Functions Locally</h3>
<p>To test and debug Edge Functions locally, you can utilize the Supabase CLI. Let's explore how to run Edge Functions locally using the Supabase CLI:</p>
<ol>
<li><p>Ensure that the Supabase server is running by executing the following command:</p>
<pre><code class="lang-bash"> supabase start
</code></pre>
</li>
<li><p>In your terminal, use the following command to serve the Edge Functions locally:</p>
<pre><code class="lang-bash"> supabase <span class="hljs-built_in">functions</span> serve
</code></pre>
<p> This command starts a local server that runs your Edge Functions, allowing you to test and debug them in a development environment.</p>
</li>
<li><p>Create the environment variables file:</p>
<pre><code class="lang-bash"> <span class="hljs-comment"># creates the file</span>
 touch .env.local
 <span class="hljs-comment"># adds the SUPABASE_URL secret</span>
 <span class="hljs-built_in">echo</span> <span class="hljs-string">"SUPABASE_URL=http://localhost:54321"</span> &gt;&gt; .env.local
 <span class="hljs-comment"># adds the SUPABASE_ANON_KEY secret</span>
 <span class="hljs-built_in">echo</span> <span class="hljs-string">"SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0"</span> &gt;&gt; .env.local
 <span class="hljs-comment">#Alternatively, you can open in your editor:</span>
 open .env.local
</code></pre>
</li>
<li><p>To run the tests, use the following command in your terminal:</p>
<pre><code class="lang-bash"> <span class="hljs-comment">#Place the .env file in the same directory as deno-test.ts</span>
 deno <span class="hljs-built_in">test</span> --allow-all deno-test.ts
</code></pre>
<p> Here's an example of what this would look like:</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1688508225738/09b364ce-1fd7-419e-a11a-bf563527df97.png" alt="Example of running tests" /></p>
</li>
</ol>
<h3 id="heading-conclusion">Conclusion</h3>
<p>In this blog post, we covered the process of setting up Supabase and PostgreSQL for backend development. We explored the concept of Supabase Edge Functions and their advantages in extending the functionality of your application. We also walked through the process of writing a simple "Hello-world" function and discussed the importance of testing Supabase Edge Functions for ensuring their correctness and performance. Lastly, we learned how to run Edge Functions locally using the Supabase CLI. By incorporating Supabase and PostgreSQL into your backend stack and thoroughly testing your Edge Functions, you can maintain the reliability and functionality of your application.</p>
<p>To learn more about Supabase and its features, consult the official <a target="_blank" href="https://supabase.com/docs">Supabase documentation</a>. For information on Deno and its testing framework, refer to the <a target="_blank" href="https://deno.land/manual">Deno documentation</a>. Additionally, you can explore the <a target="_blank" href="https://github.com/supabase/supabase">Supabase GitHub repository</a>.</p>
]]></content:encoded></item></channel></rss>