<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ supabase - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ supabase - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Thu, 18 Jun 2026 23:23:48 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/supabase/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Production-Ready Feature Flag System with Next.js and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development. In ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-production-ready-feature-flag-system-with-nextjs-and-supabase/</link>
                <guid isPermaLink="false">69851c4ec8140c13f9fa09c8</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[   feature flags ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Thu, 05 Feb 2026 22:40:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770312675718/c462d3b5-5369-45e0-ad47-c91b441fe96f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development.</p>
<p>In this article, we’ll build a real, production-ready feature flag system, not just a simple boolean toggle.</p>
<p>Specifically, we’ll implement:</p>
<ul>
<li><p>A global on/off flag to enable or disable features instantly</p>
</li>
<li><p>User-specific flags to grant access to individual users (for beta testing or internal users)</p>
</li>
<li><p>Percentage-based rollouts to gradually expose features to a subset of users</p>
</li>
<li><p>A React-powered admin dashboard to manage flags without redeploying</p>
</li>
<li><p>Client-side and server-side enforcement, so features are gated consistently everywhere</p>
</li>
</ul>
<p>By the end, we’ll finish by wiring a real Todo feature behind a feature flag, showing how entire pages and components can be safely toggled on and off in production.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-feature-flags">What Are Feature Flags?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-database-schema-design">Database Schema Design</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-supabase">Setting Up Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-react-query">Setting Up React Query</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-react-hook">Creating the React Hook</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-admin-dashboard">Building the Admin Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-a-real-world-example">Implementing a Real-World Example</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-server-side-usage">Server-Side Usage</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-react-query">Why React Query?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>A basic understanding of React and Next.js</p>
</li>
<li><p>Familiarity with TypeScript</p>
</li>
<li><p>A Supabase account (free tier works perfectly)</p>
</li>
<li><p>A code editor like VS Code</p>
</li>
<li><p>Basic understanding of React Query (TanStack Query) for server state management</p>
</li>
</ul>
<h2 id="heading-what-are-feature-flags">What Are Feature Flags?</h2>
<p>Feature flags (also called feature toggles) are configuration mechanisms that let you enable or disable features in your application without changing code. Think of them as light switches for your features.</p>
<p>Here are some common use cases:</p>
<ul>
<li><p><strong>Gradual rollouts</strong>: Release a feature to 10% of users first, then gradually increase</p>
</li>
<li><p><strong>User-specific access</strong>: Enable features for beta testers or VIP users</p>
</li>
<li><p><strong>Emergency kill switches</strong>: Instantly disable a feature if something goes wrong</p>
</li>
<li><p><strong>A/B testing</strong>: Test different versions of features with different user groups</p>
</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new Next.js project with TypeScript:</p>
<pre><code class="lang-bash">npx create-next-app@latest supabase-feature-flag --typescript --tailwind --app
<span class="hljs-built_in">cd</span> supabase-feature-flag
</code></pre>
<p>Next, install the required dependencies:</p>
<pre><code class="lang-bash">npm install @supabase/ssr @supabase/supabase-js @tanstack/react-query
</code></pre>
<p>The <code>@supabase/ssr</code> package provides server-side rendering support for Supabase, which is essential for Next.js App Router. <code>@tanstack/react-query</code> provides powerful server state management with automatic caching, invalidation, and real-time updates, which is perfect for feature flags that need to reflect changes immediately without page refreshes.</p>
<h2 id="heading-database-schema-design">Database Schema Design</h2>
<p>Before writing any code, you need to design your database schema. A feature flag needs several properties:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945095969/86610ba1-e0c8-4c0c-a500-8cdc061555ba.webp" alt="Database-schema-design" class="image--center mx-auto" width="2300" height="1246" loading="lazy"></p>
<ul>
<li><p>A unique key to identify the flag</p>
</li>
<li><p>A name and description for human readability</p>
</li>
<li><p>An enabled/disabled state</p>
</li>
<li><p>Support for user-specific access</p>
</li>
<li><p>Support for percentage-based rollouts</p>
</li>
</ul>
<p>Here's the SQL migration that creates the <code>feature_flags</code> table:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<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> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</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>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_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-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</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-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);
</code></pre>
<p>Let's break down each field:</p>
<ul>
<li><p><code>id</code>: A unique identifier for each flag</p>
</li>
<li><p><code>key</code>: A unique string identifier (like "new-dashboard" or "beta-feature")</p>
</li>
<li><p><code>name</code>: A human-readable name</p>
</li>
<li><p><code>description</code>: Optional description of what the flag controls</p>
</li>
<li><p><code>enabled</code>: Global on/off switch</p>
</li>
<li><p><code>enabled_for_users</code>: JSON array of user IDs who have access</p>
</li>
<li><p><code>enabled_for_percent</code>: Percentage of users who should see the feature (0-100)</p>
</li>
<li><p><code>metadata</code>: Flexible JSON field for additional configuration</p>
</li>
<li><p><code>created_at</code> and <code>updated_at</code>: Timestamps for tracking</p>
</li>
</ul>
<p>The migration also includes indexes for performance:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create indexes for fast lookups</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> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</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> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);
</code></pre>
<p>Indexes on <code>key</code> and <code>enabled</code> ensure fast queries when checking flag status.</p>
<h2 id="heading-setting-up-supabase">Setting Up Supabase</h2>
<h3 id="heading-step-1-create-a-supabase-project">Step 1: Create a Supabase Project</h3>
<p>To start, go to <a target="_blank" href="http://supabase.com">supabase.com</a> and sign up or log in. Then click on "New Project". Fill in your project details and wait for it to initialize.</p>
<h3 id="heading-step-2-run-the-migration">Step 2: Run the Migration</h3>
<p>In your Supabase dashboard, navigate to the SQL Editor:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945875099/72595159-3301-422e-a648-49602c4088ec.png" alt="Supabase-row-level-security(RLS)-policies" class="image--center mx-auto" width="3020" height="1650" loading="lazy"></p>
<p>Then click "New Query". Copy and paste the complete migration SQL (including the indexes and RLS policies shown below) and click "Run".</p>
<p>Here's the complete migration with Row Level Security (RLS) policies:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<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> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</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>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_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-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</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-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-comment">-- Create indexes</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> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</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> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);

<span class="hljs-comment">-- Auto-update updated_at timestamp</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> update_updated_at_column()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  NEW.updated_at = <span class="hljs-keyword">NOW</span>();
  RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ language 'plpgsql';

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> update_feature_flags_updated_at
  <span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">ON</span> feature_flags
  <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> update_updated_at_column();

<span class="hljs-comment">-- Enable Row Level Security</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> feature_flags <span class="hljs-keyword">ENABLE</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">LEVEL</span> <span class="hljs-keyword">SECURITY</span>;

<span class="hljs-comment">-- Policy: Allow public read access</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public read access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">SELECT</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>);

<span class="hljs-comment">-- Policy: Allow public write access (for admin operations)</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public write access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">ALL</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>)
  <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">CHECK</span> (<span class="hljs-literal">true</span>);
</code></pre>
<p>The RLS policies allow:</p>
<ul>
<li><p>Public read access: Anyone can check if a feature flag is enabled</p>
</li>
<li><p>Public write access: Allows the admin dashboard to create/update flags (in production, you'd restrict this further)</p>
</li>
</ul>
<h3 id="heading-step-3-get-your-api-credentials">Step 3: Get Your API Credentials</h3>
<p>Go to Settings and then API in your Supabase project. Copy your Project URL and Publishable Key. THen create a <code>.env.local</code> file in your project root:</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your_publishable_key
</code></pre>
<h2 id="heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</h2>
<p>Now let's build the core logic for checking feature flags. You'll create separate utilities for client-side and server-side usage.</p>
<h3 id="heading-typescript-types">TypeScript Types</h3>
<p>First, define the types you'll use throughout the application:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// types/feature-flag.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlag {
  id: <span class="hljs-built_in">string</span>;
  key: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  description: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  enabled: <span class="hljs-built_in">boolean</span>;
  enabled_for_users: <span class="hljs-built_in">string</span>[];
  enabled_for_percent: <span class="hljs-built_in">number</span>;
  metadata: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
  created_at: <span class="hljs-built_in">string</span>;
  updated_at: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlagCheckResult {
  enabled: <span class="hljs-built_in">boolean</span>;
  reason?: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>The <code>FeatureFlag</code> interface matches your database schema. The <code>FeatureFlagCheckResult</code> includes a <code>reason</code> field that explains why a flag is enabled or disabled, which is useful for debugging.</p>
<h3 id="heading-supabase-client-setup">Supabase Client Setup</h3>
<p>Create the Supabase client for client-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/client.ts</span>
<span class="hljs-keyword">import</span> { createBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">() =&gt;</span>
  createBrowserClient(supabaseUrl!, supabaseKey!);
</code></pre>
<p>The <code>createBrowserClient</code> function from <code>@supabase/ssr</code> creates a client optimized for browser usage.</p>
<p>For server-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/server.ts</span>
<span class="hljs-keyword">import</span> { createServerClient, <span class="hljs-keyword">type</span> CookieOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">(<span class="hljs-params">cookieStore: ReturnType&lt;<span class="hljs-keyword">typeof</span> cookies&gt;</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> createServerClient(
    supabaseUrl!,
    supabaseKey!,
    {
      cookies: {
        getAll() {
          <span class="hljs-keyword">return</span> cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          <span class="hljs-keyword">try</span> {
            cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
              cookieStore.set(name, value, options)
            )
          } <span class="hljs-keyword">catch</span> {
            <span class="hljs-comment">// The `setAll` method was called from a Server Component.</span>
            <span class="hljs-comment">// This can be ignored if you have middleware refreshing</span>
            <span class="hljs-comment">// user sessions.</span>
          }
        },
      },
    },
  );
};
</code></pre>
<p>This server client handles cookies properly for Next.js server components and API routes.</p>
<h3 id="heading-client-side-feature-flag-logic">Client-Side Feature Flag Logic</h3>
<p>Create the client-side utility for checking feature flags:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/client.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-comment">// Simple cache with 5 second TTL (React Query handles primary caching)</span>
<span class="hljs-comment">// This cache is just for reducing redundant calls within a very short window</span>
<span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, { data: FeatureFlag | <span class="hljs-literal">null</span>; expires: <span class="hljs-built_in">number</span> }&gt;();
<span class="hljs-keyword">const</span> CACHE_TTL = <span class="hljs-number">5000</span>; <span class="hljs-comment">// 5 seconds - short enough to not interfere with React Query invalidation</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span> | <span class="hljs-title">undefined</span> </span>{
  <span class="hljs-keyword">const</span> cached = cache.get(key);
  <span class="hljs-keyword">if</span> (cached &amp;&amp; cached.expires &gt; <span class="hljs-built_in">Date</span>.now()) {
    <span class="hljs-keyword">return</span> cached.data;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, data: FeatureFlag | <span class="hljs-literal">null</span></span>): <span class="hljs-title">void</span> </span>{
  cache.set(key, { data, expires: <span class="hljs-built_in">Date</span>.now() + CACHE_TTL });
}
</code></pre>
<p>The cache reduces database queries by storing flag data in memory for 5 seconds. This is a secondary cache layer – React Query handles the primary caching and automatic invalidation, ensuring changes reflect immediately across all components.</p>
<p>The <code>getFeatureFlag</code> function fetches a flag from the database:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cached = getCached(key);
  <span class="hljs-keyword">if</span> (cached !== <span class="hljs-literal">undefined</span>) <span class="hljs-keyword">return</span> cached;

  <span class="hljs-keyword">const</span> supabase = createClient();
  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) {
      setCached(key, <span class="hljs-literal">null</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  setCached(key, data);
  <span class="hljs-keyword">return</span> data;
}
</code></pre>
<p>The function first checks the cache. If the flag isn't cached, it queries Supabase. The error code <code>PGRST116</code> means "not found." In that case, you cache <code>null</code> to avoid repeated queries for non-existent flags.</p>
<p>The core logic is in <code>isFeatureEnabled</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}
</code></pre>
<p>The function follows this logic:</p>
<ol>
<li><p><strong>Flag doesn't exist</strong>: Return disabled</p>
</li>
<li><p><strong>Flag is globally disabled</strong>: Return disabled</p>
</li>
<li><p><strong>User-specific list exists</strong>: Check if the user is in the list</p>
</li>
<li><p><strong>Percentage rollout is set</strong>: Use a hash function to assign users to buckets deterministically</p>
</li>
<li><p><strong>Otherwise</strong>: Flag is globally enabled</p>
</li>
</ol>
<p>The hash function ensures consistent assignment, so that the same user always gets the same result:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>This creates a deterministic hash, so <code>simpleHash("user-123")</code> always returns the same number, ensuring consistent feature flag decisions.</p>
<h3 id="heading-server-side-feature-flag-logic">Server-Side Feature Flag Logic</h3>
<p>The server-side version is similar but uses the server Supabase client:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/server.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
  <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

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

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>The logic is identical to the client version, but it uses the server Supabase client that handles cookies correctly.</p>
<h2 id="heading-setting-up-react-query">Setting Up React Query</h2>
<p>We’ll rely heavily on React Query throughout this tutorial because feature flags are server-driven values that can change at runtime while users are actively using the application.</p>
<p>React Query provides robust server-state management through caching, background refetching, and cache invalidation. This allows feature flag changes to propagate automatically across the app without forcing page refreshes or manual state synchronization.</p>
<h3 id="heading-create-the-query-provider">Create the Query Provider</h3>
<p>First, create the <code>providers/QueryProvider.tsx</code> file to configure and initialize React Query for the entire application:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { QueryClient, QueryClientProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">QueryProvider</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
  <span class="hljs-keyword">const</span> [queryClient] = useState(
    <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">new</span> QueryClient({
        defaultOptions: {
          queries: {
            <span class="hljs-comment">// With SSR, we usually want to set some default staleTime</span>
            <span class="hljs-comment">// above 0 to avoid refetching immediately on the client</span>
            staleTime: <span class="hljs-number">20</span> * <span class="hljs-number">1000</span>,
            refetchOnWindowFocus: <span class="hljs-literal">true</span>,
            refetchOnReconnect: <span class="hljs-literal">true</span>,
          },
        },
      })
  );

  <span class="hljs-keyword">return</span> (
    &lt;QueryClientProvider client={queryClient}&gt;{children}&lt;/QueryClientProvider&gt;
  );
}
</code></pre>
<p>What’s happening in this code:</p>
<ul>
<li><p>This file runs on the client because React Query relies on React hooks, which only execute in the browser.</p>
</li>
<li><p>A single QueryClient instance is created and stored in state so it persists across renders and isn’t recreated.</p>
</li>
<li><p>The QueryClient defines how server data is cached, when it becomes stale, and when it should be refetched.</p>
</li>
</ul>
<p>This is important because components no longer need to handle fetch logic, loading states, or caching manually. Also, server data is shared and reused across components instead of being refetched repeatedly. And feature flag updates propagate automatically, keeping the app consistent without manual refreshes.</p>
<h3 id="heading-add-provider-to-root-layout">Add Provider to Root Layout</h3>
<p>Next, update the <code>app/layout.tsx</code> file to wrap the application with the React Query provider.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>
<span class="hljs-keyword">import</span> { QueryProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/providers/QueryProvider'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  title: <span class="hljs-string">'Feature Flag System'</span>,
  description: <span class="hljs-string">'Production-ready feature flag system with Next.js and Supabase'</span>,
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: {
  children: React.ReactNode
}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">"en"</span>&gt;
      &lt;body&gt;
        &lt;QueryProvider&gt;{children}&lt;/QueryProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}
</code></pre>
<p>We wrap the entire application with <code>QueryProvider</code>, so React Query can manage server data globally across the app.</p>
<h4 id="heading-what-queryprovider-actually-does">What <code>QueryProvider</code> actually does</h4>
<p><code>QueryProvider</code> creates and shares a single Query Client that is responsible for:</p>
<ul>
<li><p>Caching data fetched from the server</p>
</li>
<li><p>Tracking loading and error states</p>
</li>
<li><p>Automatically refetching data when needed</p>
</li>
<li><p>Synchronizing data between components</p>
</li>
</ul>
<p>By wrapping the app, every component inside it can use React Query hooks without any extra setup.</p>
<h2 id="heading-creating-the-react-hook">Creating the React Hook</h2>
<p>Create the <code>hooks/useFeatureFlag.ts</code> file to check whether a feature flag is enabled for a user, using React Query to cache and share the data across components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, userId?: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> {
    data: result = { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Loading...'</span> },
    isLoading: loading,
    error,
  } = useQuery&lt;FeatureFlagCheckResult&gt;({
    queryKey: [<span class="hljs-string">'featureFlag'</span>, key, userId],
    queryFn: <span class="hljs-function">() =&gt;</span> isFeatureEnabled(key, userId),
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
    refetchOnWindowFocus: <span class="hljs-literal">true</span>,
  });

  <span class="hljs-keyword">return</span> { ...result, loading, error };
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>'use client';</code> ensures this file runs in the browser because React Query hooks can only run on the client.</p>
</li>
<li><p><code>useQuery</code> fetches the feature flag status from the server and caches the result.</p>
</li>
<li><p><code>queryKey: ['featureFlag', key, userId]</code> uniquely identifies this query so React Query can cache it separately for each feature and user.</p>
</li>
<li><p><code>queryFn: () =&gt; isFeatureEnabled(key, userId)</code> is the function that actually checks if the feature is enabled.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
<li><p><code>refetchOnWindowFocus: true</code> automatically refetches the data when the user switches back to the tab.</p>
</li>
<li><p><code>return { ...result, loading, error }</code> makes it easy for components to access the flag status, loading state, and any errors.</p>
</li>
</ul>
<h3 id="heading-admin-hooks-for-managing-flags">Admin Hooks for Managing Flags</h3>
<p>We’re going to create a set of React Query hooks to manage feature flags from the admin dashboard. These hooks allow you to fetch, create, update, and delete feature flags while automatically keeping your UI in sync.</p>
<h4 id="heading-step-1-create-the-hooks-file">Step 1: Create the hooks file</h4>
<p>Start by creating a new file at <code>hooks/useFeatureFlags.ts</code> file. This is where we’ll implement all the hooks for the admin to manage feature flags.</p>
<p>After creating the <code>hooks/useFeatureFlags.ts</code> file, import React Query hooks and the FeatureFlag type so we can fetch, update, create, and delete feature flags with type safety and caching.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery, useMutation, useQueryClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>useQuery</code> fetches and caches server data automatically.</p>
</li>
<li><p><code>useMutation</code> sends updates, creates, or deletes data on the server.</p>
</li>
<li><p><code>useQueryClient</code> gives access to the query cache so we can invalidate or update queries after mutations.</p>
</li>
<li><p><code>FeatureFlag</code> is the TypeScript type definition for feature flags, ensuring our hooks use correct data structures.</p>
</li>
</ul>
<h4 id="heading-step-2-add-a-hook-to-fetch-all-feature-flags">Step 2: Add a hook to fetch all feature flags</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to fetch all feature flags for the admin page. This hook will allow components to retrieve the list of flags and automatically cache the data using React Query.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlags</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> useQuery&lt;FeatureFlag[]&gt;({
    queryKey: [<span class="hljs-string">'featureFlags'</span>],
    queryFn: <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</span>);
      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data || [];
    },
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useQuery</code> fetches all feature flags from the server and caches them automatically.</p>
</li>
<li><p><code>queryKey: ['featureFlags']</code> uniquely identifies this query so React Query can manage caching and refetching.</p>
</li>
<li><p><code>queryFn</code> is an async function that calls the <code>/api/feature-flags</code> endpoint and returns the data.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
</ul>
<p>This matters because admin components always display the latest flags without manual refresh. Also, cached data reduces unnecessary network requests. Finally, any component using this hook will automatically update when the flags change.</p>
<h4 id="heading-step-3-add-a-hook-to-update-a-feature-flag">Step 3: Add a hook to update a feature flag</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to update an existing feature flag. This hook will allow the admin to modify a flag and ensure all components using it get the updated value automatically.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useUpdateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> ({
      key,
      updates,
    }: {
      key: <span class="hljs-built_in">string</span>;
      updates: Partial&lt;FeatureFlag&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'PATCH'</span>,
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(updates),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to update feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">data, variables</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({
        queryKey: [<span class="hljs-string">'featureFlag'</span>, variables.key],
      });
      <span class="hljs-comment">// Invalidate all feature flag checks (in case userId was involved)</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to update a feature flag on the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a PATCH request to <code>/api/feature-flags/${key}</code> with the updated data.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful update to <strong>invalidate cached queries</strong> so the latest data is available everywhere:</p>
<ul>
<li><p><code>['featureFlags']</code> updates the full list of flags.</p>
</li>
<li><p><code>['featureFlag', variables.key]</code> updates the specific flag that was changed.</p>
</li>
<li><p><code>['featureFlag']</code> updates any other cached flag checks (for example, per user checks).</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-4-add-a-hook-to-delete-a-feature-flag">Step 4: Add a hook to delete a feature flag</h4>
<p>Next, add the <code>useDeleteFeatureFlag</code> hook to delete a feature flag. This hook allows the admin to remove a flag from the system and ensures the UI updates automatically everywhere the flag was used.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDeleteFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (key: <span class="hljs-built_in">string</span>) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'DELETE'</span>,
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to delete feature flag'</span>);
      }
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">_, key</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>, key] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to delete a feature flag from the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a DELETE request to <code>/api/feature-flags/${key}</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after the flag is successfully deleted to <strong>invalidate the cache</strong> so the UI updates:</p>
<ul>
<li><p><code>['featureFlags']</code> refetches the full list of flags.</p>
</li>
<li><p><code>['featureFlag', key]</code> removes the deleted flag from any cached queries.</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-5-add-a-hook-to-create-a-new-feature-flag">Step 5: Add a hook to create a new feature flag</h4>
<p>Finally, add the <code>useCreateFeatureFlag</code> hook to create a new feature flag. This allows the admin to add new flags and ensures the dashboard updates automatically when a new flag is created.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useCreateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (flag: {
      key: <span class="hljs-built_in">string</span>;
      name: <span class="hljs-built_in">string</span>;
      description?: <span class="hljs-built_in">string</span>;
      enabled?: <span class="hljs-built_in">boolean</span>;
      enabled_for_users?: <span class="hljs-built_in">string</span>[];
      enabled_for_percent?: <span class="hljs-built_in">number</span>;
      metadata?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</span>, {
        method: <span class="hljs-string">'POST'</span>,
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
        body: <span class="hljs-built_in">JSON</span>.stringify(flag),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">const</span> error = <span class="hljs-keyword">await</span> response.json();
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(error.error || <span class="hljs-string">'Failed to create feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to send a new feature flag to the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that posts the flag data to <code>/api/feature-flags</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful creation to <strong>invalidate the cached list of flags</strong>, so the admin dashboard shows the new flag immediately.</p>
</li>
</ul>
<h3 id="heading-feature-flag-gate-component">Feature Flag Gate Component</h3>
<p>Now we’ll create a wrapper component to conditionally render UI based on feature flags. This helps you show or hide parts of your app depending on whether a flag is enabled for a user.</p>
<p>Create the <code>components/FeatureFlagGate.tsx</code> file and add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { ReactNode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlag'</span>;

<span class="hljs-keyword">interface</span> FeatureFlagGateProps {
  flagKey: <span class="hljs-built_in">string</span>;
  userId?: <span class="hljs-built_in">string</span>;
  children: ReactNode;
  fallback?: ReactNode;
  showLoading?: ReactNode;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagGate</span>(<span class="hljs-params">{
  flagKey,
  userId,
  children,
  fallback = <span class="hljs-literal">null</span>,
  showLoading = <span class="hljs-literal">null</span>,
}: FeatureFlagGateProps</span>) </span>{
  <span class="hljs-keyword">const</span> { enabled, loading } = useFeatureFlag(flagKey, userId);

  <span class="hljs-keyword">if</span> (loading &amp;&amp; showLoading !== <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> &lt;&gt;{showLoading}&lt;/&gt;;
  }

  <span class="hljs-keyword">if</span> (!enabled) {
    <span class="hljs-keyword">return</span> &lt;&gt;{fallback}&lt;/&gt;;
  }

  <span class="hljs-keyword">return</span> &lt;&gt;{children}&lt;/&gt;;
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useFeatureFlag(flagKey, userId)</code> checks whether the feature is enabled for a specific user and tracks loading state.</p>
</li>
<li><p><code>loading &amp;&amp; showLoading !== null</code>: if the data is still loading, render the optional <code>showLoading</code> UI.</p>
</li>
<li><p><code>!enabled</code>: if the feature is disabled, render the optional <code>fallback</code> UI.</p>
</li>
<li><p><code>children</code> renders the actual content only if the flag is enabled and not loading.</p>
</li>
</ul>
<p>This makes it easy to conditionally render features without scattering logic throughout your components. It also supports custom loading and fallback UI for better user experience. And it works with React Query caching automatically, so flag changes propagate immediately.</p>
<h2 id="heading-building-the-admin-dashboard">Building the Admin Dashboard</h2>
<p>Admins need a way to manage feature flags in your app. To do this, we’ll create API routes that support CRUD operations (Create, Read, Update, Delete). These routes will interact with Supabase to store and modify flag data.</p>
<h3 id="heading-step-1-create-the-main-feature-flags-api-route">Step 1: Create the main feature flags API route</h3>
<p>Start by creating the <code>app/api/feature-flags/route.ts</code> file. This file will handle fetching all feature flags (GET) and creating new ones (POST).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .select(<span class="hljs-string">'*'</span>)
      .order(<span class="hljs-string">'created_at'</span>, { ascending: <span class="hljs-literal">false</span> });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}

<span class="hljs-keyword">export</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">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">if</span> (!body.key || !body.name) {
      <span class="hljs-keyword">return</span> NextResponse.json(
        { error: <span class="hljs-string">'key and name are required'</span> },
        { status: <span class="hljs-number">400</span> }
      );
    }

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .insert({
        key: body.key,
        name: body.name,
        description: body.description || <span class="hljs-literal">null</span>,
        enabled: body.enabled || <span class="hljs-literal">false</span>,
        enabled_for_users: body.enabled_for_users || [],
        enabled_for_percent: body.enabled_for_percent || <span class="hljs-number">0</span>,
        metadata: body.metadata || {},
      })
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data }, { status: <span class="hljs-number">201</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>GET</code> fetches all feature flags from Supabase, ordered by creation date.</p>
</li>
<li><p><code>POST</code> creates a new feature flag in Supabase. It checks that <code>key</code> and <code>name</code> exist, and sets defaults for optional fields.</p>
</li>
<li><p><code>createClient(cookieStore)</code> authenticates requests with <code>Supabase</code> using cookies from the client.</p>
</li>
<li><p><code>NextResponse.json()</code> sends JSON responses with data or error messages.</p>
</li>
</ul>
<h3 id="heading-step-2-create-the-dynamic-route-for-individual-flags">Step 2: Create the dynamic route for individual flags</h3>
<p>Next, Create the file <code>app/api/feature-flags/[key]/route.ts</code>. This route handles updating (PATCH) and deleting (DELETE) individual flags based on their <code>key</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PATCH</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .update({
        ...(body.name !== <span class="hljs-literal">undefined</span> &amp;&amp; { name: body.name }),
        ...(body.description !== <span class="hljs-literal">undefined</span> &amp;&amp; { description: body.description }),
        ...(body.enabled !== <span class="hljs-literal">undefined</span> &amp;&amp; { enabled: body.enabled }),
        ...(body.enabled_for_users !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_users: body.enabled_for_users,
        }),
        ...(body.enabled_for_percent !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_percent: body.enabled_for_percent,
        }),
      })
      .eq(<span class="hljs-string">'key'</span>, params.key)
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DELETE</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .delete()
      .eq(<span class="hljs-string">'key'</span>, params.key);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>PATCH</code> updates a feature flag with only the fields provided in the request body.</p>
<ul>
<li>Uses the spread operator <code>(...)</code> to include only fields that exist.</li>
</ul>
</li>
<li><p><code>DELETE</code> removes the feature flag identified by <code>key</code>.</p>
</li>
<li><p><code>eq('key', params.key)</code> ensures the operation targets the correct flag.</p>
</li>
</ul>
<h3 id="heading-step-3-create-the-admin-dashboard-ui">Step 3: Create the Admin Dashboard UI</h3>
<p>Now, create the file <code>app/admin/page.tsx</code>. This is the main admin dashboard where you can view, create, and manage feature flags.</p>
<p>With React Query, the admin dashboard becomes much cleaner and automatically updates when flags change:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagList } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/FeatureFlagList'</span>;
<span class="hljs-keyword">import</span> { CreateFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/CreateFeatureFlagModal'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlags } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AdminPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [showCreateModal, setShowCreateModal] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> { data: flags = [], isLoading: loading } = useFeatureFlags();

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">'min-h-screen bg-gray-50 py-8'</span>&gt;
      &lt;div className=<span class="hljs-string">'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'</span>&gt;
        &lt;div className=<span class="hljs-string">'mb-8 flex justify-between items-center'</span>&gt;
          &lt;div&gt;
            &lt;h1 className=<span class="hljs-string">'text-3xl font-bold text-gray-900'</span>&gt;
              Feature Flags Admin
            &lt;/h1&gt;
            &lt;p className=<span class="hljs-string">'mt-2 text-sm text-gray-600'</span>&gt;
              Manage your feature flags and rollouts
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">true</span>)}
            className=<span class="hljs-string">'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'</span>
          &gt;
            Create Feature Flag
          &lt;/button&gt;
        &lt;/div&gt;

        {loading ? (
          &lt;div className=<span class="hljs-string">'text-center py-12'</span>&gt;
            &lt;div className=<span class="hljs-string">'inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600'</span>&gt;&lt;/div&gt;
            &lt;p className=<span class="hljs-string">'mt-4 text-gray-600'</span>&gt;Loading feature flags...&lt;/p&gt;
          &lt;/div&gt;
        ) : (
          &lt;FeatureFlagList flags={flags} /&gt;
        )}

        {showCreateModal &amp;&amp; (
          &lt;CreateFeatureFlagModal
            onClose={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">false</span>)}
            onSuccess={<span class="hljs-function">() =&gt;</span> {
              setShowCreateModal(<span class="hljs-literal">false</span>);
            }}
          /&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>'use client';</code> marks this page as a client component so hooks like <code>useState</code> and React Query can run.</p>
</li>
<li><p><code>useState</code> manages whether the “Create Feature Flag” modal is open or closed.</p>
</li>
<li><p><code>useFeatureFlags()</code> fetches all feature flags from the API and keeps the list in sync automatically.</p>
</li>
<li><p><code>const { data: flags = [], isLoading: loading }</code> :</p>
<ul>
<li><p><code>flags</code> contains the feature flag list</p>
</li>
<li><p><code>loading</code> tracks whether the data is still being fetched</p>
</li>
</ul>
</li>
<li><p><code>loading ? ... : &lt;FeatureFlagList /&gt;</code> :</p>
<ul>
<li><p>Shows a loading spinner while flags are being fetched</p>
</li>
<li><p>Renders the feature flag table once data is available</p>
</li>
</ul>
</li>
<li><p><code>FeatureFlagList</code> displays all feature flags and their current states.</p>
</li>
</ul>
<h3 id="heading-using-feature-flags-mutations-in-components">Using Feature Flags Mutations in Components</h3>
<p>Earlier, we created mutation hooks for creating, updating, and deleting feature flags. Now let’s see how those hooks are actually used inside UI components to trigger updates and keep the interface in sync.</p>
<p>Here's how to use the mutation hooks in your components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>
<span class="hljs-keyword">import</span> { EditFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'./EditFeatureFlagModal'</span>
<span class="hljs-keyword">import</span> {
  useUpdateFeatureFlag,
  useDeleteFeatureFlag,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>

<span class="hljs-keyword">interface</span> FeatureFlagCardProps {
  flag: FeatureFlag
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagCard</span>(<span class="hljs-params">{ flag }: FeatureFlagCardProps</span>) </span>{
  <span class="hljs-keyword">const</span> [isEditing, setIsEditing] = useState(<span class="hljs-literal">false</span>)
  <span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag()
  <span class="hljs-keyword">const</span> deleteFlag = useDeleteFeatureFlag()

  <span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> updateFlag.mutateAsync({
        key: flag.key,
        updates: { enabled: !flag.enabled },
      })
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error toggling flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to toggle feature flag'</span>)
    }
  }

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!confirm(<span class="hljs-string">`Are you sure you want to delete "<span class="hljs-subst">${flag.name}</span>"?`</span>)) {
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> deleteFlag.mutateAsync(flag.key)
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error deleting flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to delete feature flag'</span>)
    }
  }

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;div className=<span class="hljs-string">"bg-white rounded-lg shadow p-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-start justify-between"</span>&gt;
          &lt;div className=<span class="hljs-string">"flex-1"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-3"</span>&gt;
              &lt;h3 className=<span class="hljs-string">"text-lg font-semibold text-gray-900"</span>&gt;
                {flag.name}
              &lt;/h3&gt;
              &lt;span
                className={<span class="hljs-string">`px-2 py-1 text-xs font-medium rounded-full <span class="hljs-subst">${
                  flag.enabled
                    ? <span class="hljs-string">'bg-green-100 text-green-800'</span>
                    : <span class="hljs-string">'bg-gray-100 text-gray-800'</span>
                }</span>`</span>}
              &gt;
                {flag.enabled ? <span class="hljs-string">'Enabled'</span> : <span class="hljs-string">'Disabled'</span>}
              &lt;/span&gt;
            &lt;/div&gt;
            &lt;p className=<span class="hljs-string">"mt-1 text-sm text-gray-600 font-mono"</span>&gt;{flag.key}&lt;/p&gt;
            {flag.description &amp;&amp; (
              &lt;p className=<span class="hljs-string">"mt-2 text-sm text-gray-500"</span>&gt;{flag.description}&lt;/p&gt;
            )}

            &lt;div className=<span class="hljs-string">"mt-4 flex flex-wrap gap-4 text-sm text-gray-600"</span>&gt;
              {flag.enabled_for_users.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Users:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_users.length} user(s)
                &lt;/div&gt;
              )}
              {flag.enabled_for_percent &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Rollout:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_percent}%
                &lt;/div&gt;
              )}
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div className=<span class="hljs-string">"flex items-center gap-2 ml-4"</span>&gt;
            &lt;button
              onClick={handleToggle}
              disabled={updateFlag.isPending}
              className={<span class="hljs-string">`px-3 py-1.5 text-sm font-medium rounded transition-colors <span class="hljs-subst">${
                flag.enabled
                  ? <span class="hljs-string">'bg-red-100 text-red-700 hover:bg-red-200'</span>
                  : <span class="hljs-string">'bg-green-100 text-green-700 hover:bg-green-200'</span>
              }</span> disabled:opacity-50`</span>}
            &gt;
              {updateFlag.isPending
                ? <span class="hljs-string">'...'</span>
                : flag.enabled
                ? <span class="hljs-string">'Disable'</span>
                : <span class="hljs-string">'Enable'</span>}
            &lt;/button&gt;
            &lt;button
              onClick={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">true</span>)}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-100 rounded hover:bg-blue-200 transition-colors"</span>
            &gt;
              Edit
            &lt;/button&gt;
            &lt;button
              onClick={handleDelete}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-red-700 bg-red-100 rounded hover:bg-red-200 transition-colors"</span>
            &gt;
              Delete
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      {isEditing &amp;&amp; (
        &lt;EditFeatureFlagModal
          flag={flag}
          onClose={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">false</span>)}
          onSuccess={<span class="hljs-function">() =&gt;</span> {
            setIsEditing(<span class="hljs-literal">false</span>)
          }}
        /&gt;
      )}
    &lt;/&gt;
  )
}
</code></pre>
<p>The component above works without any manual refetching or state synchronization logic. The moment a mutation succeeds, React Query automatically updates or invalidates the relevant cached data. That behavior is what enables the smooth, real-time updates you see in the UI.</p>
<p>Because all feature flag data is managed by React Query:</p>
<ul>
<li><p>When a flag is toggled, React Query automatically invalidates the relevant queries</p>
</li>
<li><p>Any component using that flag immediately receives the updated value</p>
</li>
<li><p>The admin dashboard stays in sync with the rest of the application without extra code</p>
</li>
<li><p>Loading and error states are handled consistently across all mutations.</p>
</li>
</ul>
<h2 id="heading-implementing-a-real-world-example">Implementing a Real-World Example</h2>
<p>To make all of this concrete, let’s walk through a real-world example.</p>
<p>In this section, we’ll build a simple Todo feature that is <strong>entirely controlled by a feature flag</strong>. When the flag is disabled, users see a message explaining that the feature isn’t available. When it’s enabled, the full Todo interface appears instantly without redeploying or refreshing the page.</p>
<p>This demonstrates how feature flags can safely gate entire pages or features in a live application.</p>
<p>Below is what the experience looks like as the feature flag is toggled on and off from the admin dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946162379/33b4a9e3-5284-477e-b8c5-ecc0e137e7c1.png" alt="feature-flag-disabled-page" class="image--center mx-auto" width="2996" height="1402" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946216049/edea8722-c298-4b2b-a4b5-bcc8306fb15f.png" alt="feature-flag-enabled-page" class="image--center mx-auto" width="2852" height="1536" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770299915271/54c124ac-24d5-4d3f-825c-0507675053ee.png" alt="Todo-app-with-feature-flag" class="image--center mx-auto" width="1900" height="904" loading="lazy"></p>
<h3 id="heading-step-1-create-the-todo-page-file">Step 1: Create the Todo Page File</h3>
<p>Create a new file <code>app/todos/page.tsx</code>. This page shows how to use a feature flag to conditionally render a full component. Let's build a todo app that's controlled by a feature flag. This demonstrates real-world usage.</p>
<p>At the top of the file, import the hooks we need and define the Todo interface.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagGate } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/FeatureFlagGate'</span>;

<span class="hljs-keyword">interface</span> Todo {
  id: <span class="hljs-built_in">string</span>;
  text: <span class="hljs-built_in">string</span>;
  completed: <span class="hljs-built_in">boolean</span>;
}
</code></pre>
<h3 id="heading-step-2-initialize-state-in-the-component">Step 2: Initialize state in the component</h3>
<p>Create the component and define state for todos and the input:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TodosPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [todos, setTodos] = useState&lt;Todo[]&gt;([]);
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">''</span>);
</code></pre>
<p>Here’s what’s going on<strong>:</strong></p>
<ul>
<li><p><code>todos</code> stores all todo items.</p>
</li>
<li><p><code>inputValue</code> tracks what the user types in the input field.</p>
</li>
</ul>
<h3 id="heading-step-3-load-and-save-todos-from-localstorage">Step 3: Load and save todos from localStorage</h3>
<pre><code class="lang-typescript">  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> saved = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'todos'</span>);
    <span class="hljs-keyword">if</span> (saved) setTodos(<span class="hljs-built_in">JSON</span>.parse(saved));
  }, []);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'todos'</span>, <span class="hljs-built_in">JSON</span>.stringify(todos));
  }, [todos]);
</code></pre>
<p>In this code, the first <code>useEffect</code> loads saved todos from the browser’s <code>localStorage</code> when the component mounts. The second <code>useEffect</code> saves todos whenever the list changes. This ensures your todos persist across page reloads.</p>
<h3 id="heading-step-4-add-helper-functions">Step 4: Add helper functions</h3>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> addTodo = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (inputValue.trim()) {
      setTodos([...todos, { 
        id: <span class="hljs-built_in">Date</span>.now().toString(), 
        text: inputValue.trim(), 
        completed: <span class="hljs-literal">false</span> 
      }]);
      setInputValue(<span class="hljs-string">''</span>);
    }
  };

  <span class="hljs-keyword">const</span> toggleTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.map(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  <span class="hljs-keyword">const</span> deleteTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.filter(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> todo.id !== id));
  };
</code></pre>
<p>In this code,</p>
<ul>
<li><p><code>addTodo</code> adds a new todo item with a unique <code>id</code> and resets the input.</p>
</li>
<li><p><code>toggleTodo</code> marks a todo as completed or incomplete.</p>
</li>
<li><p><code>deleteTodo</code> removes a todo from the list.</p>
</li>
</ul>
<h3 id="heading-step-5-render-the-todo-app-inside-the-featureflaggate">Step 5: Render the Todo App inside the FeatureFlagGate</h3>
<pre><code class="lang-xml">  return (
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-2xl mx-auto p-8"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-4xl font-bold mb-6"</span>&gt;</span>Todo App<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">FeatureFlagGate</span>
        <span class="hljs-attr">flagKey</span>=<span class="hljs-string">"test"</span>
        <span class="hljs-attr">fallback</span>=<span class="hljs-string">{</span>
          &lt;<span class="hljs-attr">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold mb-2"</span>&gt;</span>Feature Not Available<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Enable the "test" feature flag in the admin dashboard.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        }
      &gt;
</code></pre>
<p>What’s going on here:</p>
<ul>
<li><p>We wrap the Todo app in <code>FeatureFlagGate</code>.</p>
</li>
<li><p><code>flagKey="test"</code> only shows the Todo app if this feature flag is enabled.</p>
</li>
<li><p><code>fallback</code> displays a message when the feature is disabled.</p>
</li>
</ul>
<h3 id="heading-step-6-add-the-input-field-and-add-button">Step 6: Add the input field and Add button</h3>
<pre><code class="lang-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mb-6"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{inputValue}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInputValue(e.target.value)}
              onKeyPress={(e) =&gt; e.key === 'Enter' &amp;&amp; addTodo()}
              placeholder="What needs to be done?"
              className="flex-1 px-4 py-3 border rounded-lg"
            /&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{addTodo}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"px-6 py-3 bg-purple-600 text-white rounded-lg"</span>
            &gt;</span>
              Add
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>In this code,</p>
<ul>
<li><p>Input field captures new todo text.</p>
</li>
<li><p>Pressing Enter or clicking Add triggers <code>addTodo</code>.</p>
</li>
<li><p>It’s styled with Tailwind CSS for spacing and rounded borders.</p>
</li>
</ul>
<h3 id="heading-step-8-display-the-list-of-todos">Step 8: Display the list of todos</h3>
<pre><code class="lang-xml">
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"space-y-2"</span>&gt;</span>
          {todos.map((todo) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{todo.id}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-3 p-4 bg-gray-50 rounded-lg"</span>
            &gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> toggleTodo(todo.id)}
                className={`w-6 h-6 rounded-full border-2 ${
                  todo.completed
                    ? 'bg-green-500 border-green-500'
                    : 'border-gray-300'
                }`}
              &gt;
                {todo.completed &amp;&amp; '✓'}
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{todo.completed</span> ? '<span class="hljs-attr">line-through</span>' <span class="hljs-attr">:</span> ''}&gt;</span>
                {todo.text}
              <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> deleteTodo(todo.id)}
                className="text-red-500"
              &gt;
                Delete
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">FeatureFlagGate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  );
}
</code></pre>
<p>This code,</p>
<ul>
<li><p>Loops over <code>todos</code> to display each item.</p>
</li>
<li><p>Shows a toggle button to mark as complete, a delete button, and the todo text.</p>
</li>
<li><p>Completed todos get a <strong>line-through</strong> style.</p>
</li>
<li><p>All interaction happens <strong>inside the FeatureFlagGate</strong>, so users only see this when the flag is enabled.<br>  The entire todo app is wrapped in <code>FeatureFlagGate</code>. When the "test" flag is disabled, users see a message instead of the app. When enabled, they see the full todo interface.</p>
</li>
</ul>
<h3 id="heading-todo-app-overview">Todo App Overview</h3>
<p>This Todo app demonstrates how feature flags work in a live application. It shows how admins can enable or disable the "test" feature flag dynamically from the dashboard.</p>
<p><code>FeatureFlagGate</code> ensures the interface updates immediately when the flag changes, and entire components or pages can be toggled on or off safely using feature flags.</p>
<h2 id="heading-server-side-usage">Server-Side Usage</h2>
<p>Feature flags shouldn’t only live on the client. In many cases, you’ll want to enforce them on the server as well, especially for APIs, background jobs, or sensitive business logic.</p>
<p>In this example, we’ll protect an API route by checking a feature flag on the server before returning any data.</p>
<p>First, create the API route file <code>app/api/some-feature/route.ts</code>. This demonstrates how to check a feature flag on the server before returning data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/server'</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">request: Request</span>) </span>{
  <span class="hljs-keyword">const</span> userId = request.headers.get(<span class="hljs-string">'user-id'</span>) || <span class="hljs-literal">undefined</span>;
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> isFeatureEnabled(<span class="hljs-string">'new-feature'</span>, userId);

  <span class="hljs-keyword">if</span> (!result.enabled) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Feature not available'</span> },
      { status: <span class="hljs-number">403</span> }
    );
  }

  <span class="hljs-comment">// Feature is enabled, proceed with logic</span>
  <span class="hljs-keyword">return</span> NextResponse.json({ data: <span class="hljs-string">'Feature content'</span> });
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p>File creation: <code>app/api/some-feature/route.ts</code> defines a new API route.</p>
</li>
<li><p><code>isFeatureEnabled</code>: Checks whether the <code>'new-feature'</code> flag is active for the user.</p>
</li>
<li><p>Conditional response: Returns a <code>403</code> error if the feature is disabled. Otherwise, proceeds normally.</p>
</li>
<li><p>Server-side gating: Lets you protect entire endpoints, so users only access functionality when the feature is enabled.</p>
</li>
</ul>
<h2 id="heading-why-react-query">Why React Query?</h2>
<p>Feature flags introduce a unique challenge because they must remain consistent across the entire UI even as they change dynamically. Without a dedicated server-state solution, you’d need to manually refetch data, coordinate updates between components, and handle edge cases where parts of the UI fall out of sync.</p>
<p>React Query treats feature flags as shared server state. Once fetched, flags are cached and reused across components. When an admin updates a flag, the cache is invalidated and refetched in the background, triggering immediate UI updates everywhere the flag is used. This makes React Query a natural fit for feature flags, where correctness, consistency, and real-time updates are critical.</p>
<h3 id="heading-real-world-impact">Real-World Impact</h3>
<p>Before React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Had to manually refetch after every update</span>
<span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, { method: <span class="hljs-string">'PATCH'</span>, ... });
  fetchFlags(); <span class="hljs-comment">// Manual refetch</span>
  <span class="hljs-comment">// Other components still show old data until page refresh</span>
};
</code></pre>
<p>After React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Automatic cache invalidation - everything updates instantly</span>
<span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag();
<span class="hljs-keyword">await</span> updateFlag.mutateAsync({ key, updates });
<span class="hljs-comment">// All components using this flag automatically update!</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You’ve now built a complete, production-ready feature flag system using Next.js and Supabase. The system supports global toggles, user-specific access, and percentage-based rollouts, all backed by a flexible database schema.</p>
<p>Feature flags can be checked on both the client and the server, ensuring consistent behavior across UI components and API routes. With React Query handling caching and invalidation, changes made in the admin dashboard propagate instantly throughout the application without deploying or refreshing the page.</p>
<p>Feature flags are a foundational tool for modern development. They let you deploy code safely, test new ideas with real users, and react quickly when something goes wrong. With this setup in place, you can confidently extend the system with audit logs, analytics, scheduled rollouts, or deeper CI/CD integrations as your product grows.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build an AI-Powered RAG Search Application with Next.js, Supabase, and OpenAI ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll learn how to build a complete RAG (Retrieval-Augmented Generation) search application from scratch. Your application will allow users to upload documents, store them securely, and search through them using AI-powered semantic... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-an-ai-powered-rag-search-application-with-nextjs-supabase-and-openai/</link>
                <guid isPermaLink="false">6978f421ead51482f82901bf</guid>
                
                    <category>
                        <![CDATA[ RAG  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ openai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Mayur Vekariya ]]>
                </dc:creator>
                <pubDate>Tue, 27 Jan 2026 17:21:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1769534479648/a3f19714-a00b-4444-9289-753902282ac6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll learn how to build a complete RAG (Retrieval-Augmented Generation) search application from scratch. Your application will allow users to upload documents, store them securely, and search through them using AI-powered semantic search.</p>
<p>By the end of this guide, you'll have a fully functional application that can:</p>
<ul>
<li><p>Upload and process PDF, DOCX, and TXT files</p>
</li>
<li><p>Store documents in Supabase Storage</p>
</li>
<li><p>Generate embeddings using OpenAI</p>
</li>
<li><p>Perform semantic search across document chunks</p>
</li>
<li><p>Provide AI-generated answers based on document content</p>
</li>
<li><p>View and manage uploaded documents</p>
</li>
</ul>
<p>This is a production-ready solution that you can deploy and use immediately.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-youll-learn">What You'll Learn</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-technologies">Understanding the Technologies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-overview">Project Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-create-your-nextjs-project">Step 1: Create Your Next.js Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-install-required-dependencies">Step 2: Install Required Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-set-up-your-supabase-project">Step 3: Set Up Your Supabase Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-configure-environment-variables">Step 4: Configure Environment Variables</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-create-the-upload-api-route">Step 5: Create the Upload API Route</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-create-the-rag-search-api-route">Step 6: Create the RAG Search API Route</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-create-the-documents-api-route">Step 7: Create the Documents API Route</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-create-the-upload-modal-component">Step 8: Create the Upload Modal Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-9-create-the-pdf-viewer-modal-component">Step 9: Create the PDF Viewer Modal Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-10-create-the-navigation-component">Step 10: Create the Navigation Component</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-11-create-the-home-page-search-interface">Step 11: Create the Home Page (Search Interface)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-12-create-the-documents-page">Step 12: Create the Documents Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-13-test-your-application">Step 13: Test Your Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-14-deploy-your-application">Step 14: Deploy Your Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-rag-search-works">How RAG Search Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-troubleshooting-common-issues">Troubleshooting Common Issues</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-youll-learn"><strong>What You'll Learn</strong></h2>
<p>In this handbook, you'll learn how to:</p>
<ul>
<li><p>Set up a Next.js application with TypeScript</p>
</li>
<li><p>Configure Supabase for database and file storage</p>
</li>
<li><p>Integrate OpenAI embeddings and chat completions</p>
</li>
<li><p>Implement document text extraction and chunking</p>
</li>
<li><p>Build a vector search system using PostgreSQL</p>
</li>
<li><p>Create a modern UI with React components</p>
</li>
<li><p>Handle file uploads and storage</p>
</li>
<li><p>Implement RAG (Retrieval-Augmented Generation) search</p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed on your computer</p>
</li>
<li><p>A Supabase account (free tier works fine)</p>
</li>
<li><p>An OpenAI API key</p>
</li>
<li><p>Basic knowledge of React and TypeScript</p>
</li>
<li><p>Familiarity with Next.js (helpful but not required)</p>
</li>
</ul>
<h2 id="heading-understanding-the-technologies"><strong>Understanding the Technologies</strong></h2>
<p>Before we dive into building the application, you should understand the key technologies and concepts you'll be working with:</p>
<h3 id="heading-what-is-rag-retrieval-augmented-generation"><strong>What is RAG (Retrieval-Augmented Generation)?</strong></h3>
<p>RAG is an AI pattern that combines information retrieval with text generation. Instead of relying solely on an AI model's training data, RAG retrieves relevant information from your own documents. It then uses that information as context to generate accurate, up-to-date answers. This approach gives you:</p>
<ul>
<li><p><strong>Accuracy</strong>: Answers are based on your actual documents, not just the AI's training data</p>
</li>
<li><p><strong>Transparency</strong>: You can see which document sections were used to generate the answer</p>
</li>
<li><p><strong>Efficiency</strong>: Only relevant document chunks are used, reducing token costs</p>
</li>
</ul>
<h3 id="heading-what-are-embeddings-and-vector-database"><strong>What are Embeddings and Vector Database?</strong></h3>
<p>Embeddings are numerical representations of text that capture semantic meaning. When you convert text to an embedding, similar meanings are represented by similar numbers. For example, "dog" and "puppy" would have similar embeddings. Meanwhile, "dog" and "airplane" would have very different ones.</p>
<p>OpenAI's embedding models convert text into vectors. These are arrays of numbers that can be compared mathematically. This allows you to find documents that are semantically similar to a search query. You can find matches even if they don't contain the exact same words.</p>
<p>A vector database is a specialized database designed to store and search through embeddings efficiently. Instead of searching for exact text matches, vector databases use mathematical operations. They use operations like <a target="_blank" href="https://www.freecodecamp.org/news/how-does-cosine-similarity-work/">cosine similarity</a> to find the most semantically similar content.</p>
<p>In this tutorial, you'll use Supabase's PostgreSQL database with the <code>pgvector</code> extension. This extension adds vector storage and similarity search capabilities to PostgreSQL. This lets you store embeddings alongside your regular database data. You can also perform fast similarity searches.</p>
<h3 id="heading-what-is-text-chunking"><strong>What is Text Chunking?</strong></h3>
<p>Text chunking is the process of breaking large documents into smaller, manageable pieces. This is necessary for several reasons.</p>
<p>First, AI models have token limits. These are maximum input sizes. Second, smaller chunks allow for more precise retrieval. Third, overlapping chunks ensure context isn't lost at boundaries.</p>
<p>You'll use LangChain's <code>RecursiveCharacterTextSplitter</code>. This tool intelligently splits text while trying to preserve sentence and paragraph boundaries.</p>
<h3 id="heading-what-is-supabase"><strong>What is Supabase?</strong></h3>
<p>Supabase is an open-source Firebase alternative. It provides several key features.</p>
<p>You get a PostgreSQL database, which is a powerful, open-source relational database. You also get storage, which is file storage similar to AWS S3. There are real-time features that provide real-time subscriptions to database changes. Finally, there's built-in user authentication.</p>
<p>For this project, you'll use Supabase's database to store document chunks and embeddings. You'll also use Supabase Storage to store the original uploaded files.</p>
<h3 id="heading-what-is-tailwind-css"><strong>What is Tailwind CSS?</strong></h3>
<p>Tailwind CSS is a utility-first CSS framework that lets you style your application by applying pre-built utility classes directly in your HTML/JSX. Instead of writing custom CSS, you use classes like <code>bg-blue-600</code>, <code>text-white</code>, and <code>rounded-lg</code> to style elements.</p>
<p>You'll use Tailwind CSS in this project because it speeds up development by providing ready-made styling utilities. It also ensures consistent design across the application. Plus, it makes it easy to create responsive, modern UIs. Finally, it works seamlessly with Next.js.</p>
<p>Now that you understand the core concepts and tools we’ll be using, let's start building the application.</p>
<h2 id="heading-project-overview"><strong>Project Overview</strong></h2>
<p>Your RAG search application will consist of:</p>
<ol>
<li><p><strong>Frontend</strong>: Next.js application with React components for uploading documents and searching</p>
</li>
<li><p><strong>Backend API Routes</strong>: Next.js API routes for handling uploads, searches, and document management</p>
</li>
<li><p><strong>Database</strong>: Supabase PostgreSQL with vector extension for storing embeddings</p>
</li>
<li><p><strong>Storage</strong>: Supabase Storage for storing original files</p>
</li>
<li><p><strong>AI Integration</strong>: OpenAI for generating embeddings and chat completions</p>
</li>
</ol>
<p>The application will have two main pages:</p>
<ul>
<li><p><strong>Search Page</strong>: Where users can ask questions about their uploaded documents and get AI-generated answers</p>
</li>
<li><p><strong>Documents Page</strong>: Where users can view all uploaded documents, upload new ones, preview files, and manage their document library</p>
</li>
</ul>
<p>Let's start building!</p>
<p>If you ever get stuck on the source code, you can view it on GitHub here:</p>
<p><a target="_blank" href="https://github.com/mayur9210/rag-search-app">https://github.com/mayur9210/rag-search-app</a></p>
<h2 id="heading-step-1-create-your-nextjs-project">Step 1: Create Your Next.js Project</h2>
<p>Start by creating a new Next.js project with TypeScript. Open your terminal and run:</p>
<pre><code class="lang-bash">npx create-next-app@latest rag-search-app --typescript --tailwind --app
</code></pre>
<p>When prompted, choose the following options:</p>
<ul>
<li><p>TypeScript: Yes</p>
</li>
<li><p>ESLint: Yes</p>
</li>
<li><p>Tailwind CSS: Yes</p>
</li>
<li><p>App Router: Yes (default)</p>
</li>
<li><p>Customize import alias: No</p>
</li>
</ul>
<p>Navigate into your project directory:</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> rag-search-app
</code></pre>
<p>Now that your project is set up, you'll need to install the additional packages required for document processing, AI integration, and database operations.</p>
<h2 id="heading-step-2-install-required-dependencies">Step 2: Install Required Dependencies</h2>
<p>You'll need several packages for this project. You can install them using npm:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js @langchain/openai @langchain/textsplitters langchain openai mammoth pdf2json
</code></pre>
<p>Here's what each package does:</p>
<ul>
<li><p><code>@supabase/supabase-js</code>: Client library for interacting with Supabase (database and storage)</p>
</li>
<li><p><code>@langchain/openai</code>: LangChain integration for OpenAI (helps with text processing)</p>
</li>
<li><p><code>@langchain/textsplitters</code>: Text splitting utilities for chunking documents into smaller pieces</p>
</li>
<li><p><code>langchain</code>: Core LangChain library (provides AI workflow tools)</p>
</li>
<li><p><code>openai</code>: Official OpenAI SDK (for generating embeddings and chat completions)</p>
</li>
<li><p><code>mammoth</code>: Converts DOCX files to plain text</p>
</li>
<li><p><code>pdf2json</code>: Extracts text from PDF files</p>
</li>
</ul>
<p>Install the TypeScript types for pdf2json:</p>
<pre><code class="lang-bash">npm install --save-dev @types/pdf-parse
</code></pre>
<p>With all dependencies installed, you're ready to set up your Supabase project, which will handle your database and file storage needs.</p>
<h2 id="heading-step-3-set-up-your-supabase-project">Step 3: Set Up Your Supabase Project</h2>
<h3 id="heading-create-a-supabase-project">Create a Supabase Project</h3>
<p>First, you’ll need to create a new Supabase project, which you can do by following these steps:</p>
<ol>
<li><p>Go to <a target="_blank" href="https://supabase.com/"><strong>supabase.com</strong></a> and sign in or create an account</p>
</li>
<li><p>Click "New Project"</p>
</li>
<li><p>Fill in your project details:</p>
<ul>
<li><p>Name: <code>rag-search-app</code> (or any name you prefer)</p>
</li>
<li><p>Database Password: Choose a strong password (save this – you'll need it)</p>
</li>
<li><p>Region: Select the region closest to you</p>
</li>
</ul>
</li>
<li><p>Click "Create new project" and wait for it to be ready (this takes a few minutes)</p>
</li>
</ol>
<h3 id="heading-get-your-supabase-credentials">Get Your Supabase Credentials</h3>
<p>Once your project is ready, go to <strong>Settings</strong> and then <strong>API</strong>.</p>
<p>Copy the following values:</p>
<ul>
<li><p><strong>Project URL</strong> (this is your <code>NEXT_PUBLIC_SUPABASE_URL</code>)</p>
</li>
<li><p><strong>anon public key</strong> (this is your <code>NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY</code>)</p>
</li>
<li><p><strong>service_role key</strong> (this is your <code>SUPABASE_SERVICE_ROLE_KEY</code>)</p>
</li>
</ul>
<p><strong>Important</strong>: Keep your service role key secret. Never expose it in client-side code. It bypasses Row-Level Security (RLS) policies, which is necessary for server-side file uploads but should never be used in browser code.</p>
<h3 id="heading-set-up-the-database-schema"><strong>Set Up the Database Schema</strong></h3>
<p>Now you'll set up the database structure to store your documents and embeddings. Go to <strong>SQL Editor</strong> in your Supabase dashboard and run the following SQL:</p>
<pre><code class="lang-pgsql"><span class="hljs-comment">-- Enable the vector extension for embeddings</span>
<span class="hljs-comment">-- This extension allows PostgreSQL to store and search vector data efficiently</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">EXTENSION</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> vector;

<span class="hljs-comment">-- Create the documents table</span>
<span class="hljs-comment">-- This table stores document chunks, their metadata, and embeddings</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> documents (
  id <span class="hljs-type">BIGSERIAL</span> <span class="hljs-keyword">PRIMARY KEY</span>,
  content <span class="hljs-type">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">NULL</span>,
  metadata <span class="hljs-type">JSONB</span>,
  embedding vector(<span class="hljs-number">1536</span>)  <span class="hljs-comment">-- OpenAI's text-embedding-3-small produces 1536-dimensional vectors</span>
  file_path <span class="hljs-type">text</span> <span class="hljs-keyword">null</span>,
  file_url <span class="hljs-type">text</span> <span class="hljs-keyword">null</span>,
);

<span class="hljs-comment">-- Create an index on the embedding column for faster similarity search</span>
<span class="hljs-comment">-- The ivfflat index speeds up vector similarity queries</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">ON</span> documents <span class="hljs-keyword">USING</span> ivfflat (embedding vector_cosine_ops);

<span class="hljs-comment">-- Create a function for matching documents based on similarity</span>
<span class="hljs-comment">-- This function finds the most similar document chunks to a query embedding</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> match_documents(
  query_embedding vector(<span class="hljs-number">1536</span>),
  match_threshold <span class="hljs-type">float</span>,
  match_count <span class="hljs-type">int</span>
)
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TABLE</span> (
  id <span class="hljs-type">bigint</span>,
  content <span class="hljs-type">text</span>,
  metadata <span class="hljs-type">jsonb</span>,
  similarity <span class="hljs-type">float</span>
)
<span class="hljs-keyword">LANGUAGE</span> plpgsql
<span class="hljs-keyword">AS</span> $$<span class="pgsql">
<span class="hljs-keyword">BEGIN</span>
  <span class="hljs-keyword">RETURN QUERY</span>
  <span class="hljs-keyword">SELECT</span>
    documents.id,
    documents.content,
    documents.metadata,
    <span class="hljs-number">1</span> - (documents.embedding &lt;=&gt; query_embedding) <span class="hljs-keyword">AS</span> similarity
  <span class="hljs-keyword">FROM</span> documents
  <span class="hljs-keyword">WHERE</span> <span class="hljs-number">1</span> - (documents.embedding &lt;=&gt; query_embedding) &gt; match_threshold
  <span class="hljs-keyword">ORDER</span> <span class="hljs-keyword">BY</span> documents.embedding &lt;=&gt; query_embedding
  <span class="hljs-keyword">LIMIT</span> match_count;
<span class="hljs-keyword">END</span>;
$$</span>;
</code></pre>
<p>This SQL does the following:</p>
<ul>
<li><p><strong>Enables the vector extension</strong>: This adds vector storage and similarity search capabilities to PostgreSQL</p>
</li>
<li><p><strong>Creates the documents table</strong>: Stores document chunks, metadata (file name, type, and so on), and their embeddings</p>
</li>
<li><p><strong>Creates an index</strong>: Speeds up similarity searches on the embedding column</p>
</li>
<li><p><strong>Creates a match function</strong>: Finds the most similar document chunks to a query embedding using cosine similarity</p>
</li>
</ul>
<p>The <code>&lt;=&gt;</code> operator calculates cosine distance between vectors. A smaller distance means more similar content.</p>
<h3 id="heading-set-up-supabase-storage"><strong>Set Up Supabase Storage</strong></h3>
<p>You’ll need a storage bucket to store uploaded files. This is separate from the database and holds the original PDF, DOCX, and TXT files.</p>
<p>To set up your storage bucket:</p>
<ol>
<li><p>Go to <strong>Storage</strong> in your Supabase dashboard</p>
</li>
<li><p>Click <strong>New bucket</strong></p>
</li>
<li><p>Name it <code>documents</code></p>
</li>
<li><p>Set it to <strong>Public</strong> (this allows file downloads)</p>
</li>
<li><p>Click <strong>Create bucket</strong></p>
</li>
</ol>
<p>If you prefer a private bucket, you can use the service role key for server-side operations, which bypasses Row-Level Security policies. For this tutorial, a public bucket is simpler and works well.</p>
<p>Now that your Supabase project is configured, you'll set up your environment variables to connect your Next.js application to Supabase and OpenAI.</p>
<h2 id="heading-step-4-configure-environment-variables">Step 4: Configure Environment Variables</h2>
<p>Create a <code>.env.local</code> file in your project root:</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
OPENAI_API_KEY=your_openai_api_key
</code></pre>
<p>Replace the placeholder values with your actual credentials:</p>
<ul>
<li><p>Get Supabase values from <strong>Settings</strong> → <strong>API</strong> in your Supabase dashboard</p>
</li>
<li><p>Get your OpenAI API key from <a target="_blank" href="https://platform.openai.com/api-keys"><strong>platform.openai.com/api-keys</strong></a></p>
</li>
</ul>
<p><strong>Security Note</strong>: Never commit <code>.env.local</code> to version control. It's already in <code>.gitignore</code> by default, but double-check to ensure your secrets stay secure.</p>
<p>With your environment configured, you're ready to start building the API routes that will handle file uploads, searches, and document management.</p>
<h2 id="heading-step-5-create-the-upload-api-route">Step 5: Create the Upload API Route</h2>
<p>Now you'll create the API route that handles file uploads. This route will process uploaded files, extract their text, split them into chunks, generate embeddings, and store everything in your database and storage.</p>
<p>Create <code>src/app/api/upload/route.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">'openai'</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { RecursiveCharacterTextSplitter } <span class="hljs-keyword">from</span> <span class="hljs-string">'@langchain/textsplitters'</span>;
<span class="hljs-keyword">import</span> mammoth <span class="hljs-keyword">from</span> <span class="hljs-string">'mammoth'</span>;

<span class="hljs-keyword">const</span> url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
<span class="hljs-keyword">const</span> anonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!;
<span class="hljs-keyword">const</span> serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
<span class="hljs-keyword">const</span> supabaseStorage = createClient(url, serviceKey || anonKey);
<span class="hljs-keyword">const</span> supabase = createClient(url, anonKey);
<span class="hljs-keyword">const</span> openai = <span class="hljs-keyword">new</span> OpenAI();

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">safeDecodeURIComponent</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">string</span> </span>{
  <span class="hljs-keyword">try</span> { 
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">decodeURIComponent</span>(str); 
  } <span class="hljs-keyword">catch</span> { 
    <span class="hljs-keyword">try</span> { 
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">decodeURIComponent</span>(str.replace(<span class="hljs-regexp">/%/g</span>, <span class="hljs-string">'%25'</span>)); 
    } <span class="hljs-keyword">catch</span> { 
      <span class="hljs-keyword">return</span> str; 
    } 
  }
}

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">extractTextFromFile</span>(<span class="hljs-params">file: File</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">string</span>&gt; </span>{
  <span class="hljs-keyword">const</span> buffer = Buffer.from(<span class="hljs-keyword">await</span> file.arrayBuffer());
  <span class="hljs-keyword">const</span> fileName = file.name.toLowerCase();

  <span class="hljs-keyword">if</span> (fileName.endsWith(<span class="hljs-string">'.pdf'</span>)) {
    <span class="hljs-keyword">const</span> PDFParser = (<span class="hljs-keyword">await</span> <span class="hljs-keyword">import</span>(<span class="hljs-string">'pdf2json'</span>)).default;
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Promise</span>(<span class="hljs-function">(<span class="hljs-params">resolve, reject</span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> pdfParser = <span class="hljs-keyword">new</span> (PDFParser <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>)(<span class="hljs-literal">null</span>, <span class="hljs-literal">true</span>);
      pdfParser.on(<span class="hljs-string">'pdfParser_dataError'</span>, <span class="hljs-function">(<span class="hljs-params">err: <span class="hljs-built_in">any</span></span>) =&gt;</span> 
        reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`PDF parsing error: <span class="hljs-subst">${err.parserError}</span>`</span>))
      );
      pdfParser.on(<span class="hljs-string">'pdfParser_dataReady'</span>, <span class="hljs-function">(<span class="hljs-params">pdfData: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">let</span> fullText = <span class="hljs-string">''</span>;
          pdfData.Pages?.forEach(<span class="hljs-function">(<span class="hljs-params">page: <span class="hljs-built_in">any</span></span>) =&gt;</span> 
            page.Texts?.forEach(<span class="hljs-function">(<span class="hljs-params">text: <span class="hljs-built_in">any</span></span>) =&gt;</span> 
              text.R?.forEach(<span class="hljs-function">(<span class="hljs-params">r: <span class="hljs-built_in">any</span></span>) =&gt;</span> 
                r.T &amp;&amp; (fullText += safeDecodeURIComponent(r.T) + <span class="hljs-string">' '</span>)
              )
            )
          );
          resolve(fullText.trim());
        } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
          reject(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Error extracting text: <span class="hljs-subst">${error.message}</span>`</span>));
        }
      });
      pdfParser.parseBuffer(buffer);
    });
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (fileName.endsWith(<span class="hljs-string">'.docx'</span>)) {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> mammoth.extractRawText({ buffer });
    <span class="hljs-keyword">return</span> result.value;
  } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (fileName.endsWith(<span class="hljs-string">'.txt'</span>)) {
    <span class="hljs-keyword">return</span> buffer.toString(<span class="hljs-string">'utf-8'</span>);
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Unsupported file type. Please upload PDF, DOCX, or TXT files.'</span>);
  }
}

<span class="hljs-keyword">export</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">req: Request</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> file = (<span class="hljs-keyword">await</span> req.formData()).get(<span class="hljs-string">'file'</span>) <span class="hljs-keyword">as</span> File;
    <span class="hljs-keyword">if</span> (!file) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">'No file provided'</span> }, { status: <span class="hljs-number">400</span> });
    }

    <span class="hljs-keyword">const</span> documentId = crypto.randomUUID();
    <span class="hljs-keyword">const</span> uploadDate = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString();
    <span class="hljs-keyword">const</span> filePath = <span class="hljs-string">`<span class="hljs-subst">${documentId}</span>.<span class="hljs-subst">${file.name.split(<span class="hljs-string">'.'</span>).pop() || <span class="hljs-string">'bin'</span>}</span>`</span>;

    <span class="hljs-comment">// Upload file to Supabase Storage</span>
    <span class="hljs-keyword">const</span> fileBuffer = Buffer.from(<span class="hljs-keyword">await</span> file.arrayBuffer());
    <span class="hljs-keyword">const</span> { error: storageError } = <span class="hljs-keyword">await</span> supabaseStorage.storage
      .from(<span class="hljs-string">'documents'</span>)
      .upload(filePath, fileBuffer, {
        contentType: file.type || <span class="hljs-string">'application/octet-stream'</span>,
        upsert: <span class="hljs-literal">false</span>,
      });

    <span class="hljs-keyword">if</span> (storageError) {
      <span class="hljs-keyword">const</span> msg = storageError.message || <span class="hljs-string">'Unknown storage error'</span>;
      <span class="hljs-keyword">if</span> (msg.includes(<span class="hljs-string">'row-level security'</span>) || msg.includes(<span class="hljs-string">'RLS'</span>)) {
        <span class="hljs-keyword">return</span> NextResponse.json({ 
          success: <span class="hljs-literal">false</span>, 
          error: <span class="hljs-string">`Storage RLS error: <span class="hljs-subst">${msg}</span>. Ensure SUPABASE_SERVICE_ROLE_KEY is set.`</span> 
        }, { status: <span class="hljs-number">500</span> });
      }
      <span class="hljs-keyword">return</span> NextResponse.json({ 
        success: <span class="hljs-literal">false</span>, 
        error: <span class="hljs-string">`Failed to store file: <span class="hljs-subst">${msg}</span>`</span> 
      }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-comment">// Get public URL for the file</span>
    <span class="hljs-keyword">const</span> { data: urlData } = supabaseStorage.storage
      .from(<span class="hljs-string">'documents'</span>)
      .getPublicUrl(filePath);

    <span class="hljs-comment">// Extract text from file</span>
    <span class="hljs-keyword">const</span> text = <span class="hljs-keyword">await</span> extractTextFromFile(file);
    <span class="hljs-keyword">if</span> (!text || text.trim().length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">return</span> NextResponse.json({ 
        error: <span class="hljs-string">'Could not extract text from file'</span> 
      }, { status: <span class="hljs-number">400</span> });
    }

    <span class="hljs-comment">// Split text into chunks</span>
    <span class="hljs-comment">// Chunk size of 800 characters with 100-character overlap ensures</span>
    <span class="hljs-comment">// we don't lose context at chunk boundaries</span>
    <span class="hljs-keyword">const</span> textSplitter = <span class="hljs-keyword">new</span> RecursiveCharacterTextSplitter({
      chunkSize: <span class="hljs-number">800</span>,
      chunkOverlap: <span class="hljs-number">100</span>,
    });
    <span class="hljs-keyword">const</span> chunks = <span class="hljs-keyword">await</span> textSplitter.splitText(text);

    <span class="hljs-comment">// Process each chunk: generate embedding and store in database</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; chunks.length; i++) {
      <span class="hljs-keyword">const</span> chunk = chunks[i];

      <span class="hljs-comment">// Generate embedding using OpenAI</span>
      <span class="hljs-comment">// This converts the text chunk into a 1536-dimensional vector</span>
      <span class="hljs-keyword">const</span> emb = <span class="hljs-keyword">await</span> openai.embeddings.create({
        model: <span class="hljs-string">'text-embedding-3-small'</span>,
        input: chunk,
      });

      <span class="hljs-comment">// Store chunk with embedding in database</span>
      <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">'documents'</span>).insert({
        content: chunk,
        metadata: { 
          source: file.name,
          document_id: documentId,
          file_name: file.name,
          file_type: file.type || file.name.split(<span class="hljs-string">'.'</span>).pop(),
          file_size: file.size,
          upload_date: uploadDate,
          chunk_index: i,
          total_chunks: chunks.length,
          file_path: filePath,
          file_url: urlData.publicUrl,
        },
        embedding: <span class="hljs-built_in">JSON</span>.stringify(emb.data[<span class="hljs-number">0</span>].embedding),
      });

      <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-keyword">return</span> NextResponse.json({ 
          success: <span class="hljs-literal">false</span>, 
          error: error.message 
        }, { status: <span class="hljs-number">500</span> });
      }
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ 
      success: <span class="hljs-literal">true</span>, 
      documentId, 
      fileName: file.name, 
      chunks: chunks.length, 
      textLength: text.length, 
      fileUrl: urlData.publicUrl 
    });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ 
      success: <span class="hljs-literal">false</span>, 
      error: error.message || <span class="hljs-string">'Failed to process file'</span> 
    }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>This route handles the complete upload workflow:</p>
<ol>
<li><p>Receives the file from the client via FormData</p>
</li>
<li><p>Generates a unique document ID using <code>crypto.randomUUID()</code></p>
</li>
<li><p>Uploads the file to Supabase Storage for safekeeping</p>
</li>
<li><p>Extracts text based on file type (PDF, DOCX, or TXT)</p>
</li>
<li><p>Splits the text into chunks of 800 characters with 100-character overlap</p>
</li>
<li><p>Generates embeddings for each chunk using OpenAI's embedding model</p>
</li>
<li><p>Stores each chunk with its embedding and metadata in the database</p>
</li>
</ol>
<p>The overlap between chunks ensures that if a sentence or concept spans a chunk boundary, it won't be lost. Now that you can upload and process documents, let's create the search functionality.</p>
<h2 id="heading-step-6-create-the-rag-search-api-route">Step 6: Create the RAG Search API Route</h2>
<p>This route implements the core RAG functionality: it takes a user's query, finds the most relevant document chunks, and uses them to generate an accurate answer.</p>
<p>Create <code>src/app/api/search/route.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> OpenAI <span class="hljs-keyword">from</span> <span class="hljs-string">'openai'</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;

<span class="hljs-keyword">const</span> supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!
);
<span class="hljs-keyword">const</span> openai = <span class="hljs-keyword">new</span> OpenAI();

<span class="hljs-keyword">export</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">req: Request</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> { query } = <span class="hljs-keyword">await</span> req.json();

    <span class="hljs-comment">// Generate embedding for the user's query</span>
    <span class="hljs-comment">// This converts the search query into the same vector space as document chunks</span>
    <span class="hljs-keyword">const</span> emb = <span class="hljs-keyword">await</span> openai.embeddings.create({ 
      model: <span class="hljs-string">'text-embedding-3-small'</span>, 
      input: query 
    });

    <span class="hljs-comment">// Find similar documents using vector similarity search</span>
    <span class="hljs-comment">// The match_documents function finds the 5 most similar chunks</span>
    <span class="hljs-keyword">const</span> { data: results, error } = <span class="hljs-keyword">await</span> supabase.rpc(<span class="hljs-string">'match_documents'</span>, {
      query_embedding: <span class="hljs-built_in">JSON</span>.stringify(emb.data[<span class="hljs-number">0</span>].embedding),
      match_threshold: <span class="hljs-number">0.0</span>,  <span class="hljs-comment">// Accept any similarity (you can increase this for stricter matching)</span>
      match_count: <span class="hljs-number">5</span>,        <span class="hljs-comment">// Return top 5 most similar chunks</span>
    });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-comment">// Combine retrieved chunks into context</span>
    <span class="hljs-comment">// These chunks will be used as context for the AI to generate an answer</span>
    <span class="hljs-keyword">const</span> context = results?.map(<span class="hljs-function">(<span class="hljs-params">r: <span class="hljs-built_in">any</span></span>) =&gt;</span> r.content).join(<span class="hljs-string">'\n---\n'</span>) || <span class="hljs-string">''</span>;

    <span class="hljs-comment">// Generate answer using OpenAI with retrieved context</span>
    <span class="hljs-comment">// This is the "Generation" part of RAG</span>
    <span class="hljs-keyword">const</span> completion = <span class="hljs-keyword">await</span> openai.chat.completions.create({
      model: <span class="hljs-string">'gpt-4o-mini'</span>,
      messages: [
        { 
          role: <span class="hljs-string">'system'</span>, 
          content: <span class="hljs-string">'You are a helpful assistant. Use the provided context to answer questions. If the answer is not in the context, say you do not know.'</span> 
        },
        { 
          role: <span class="hljs-string">'user'</span>, 
          content: <span class="hljs-string">`Context: <span class="hljs-subst">${context}</span>\n\nQuestion: <span class="hljs-subst">${query}</span>`</span> 
        }
      ],
    });

    <span class="hljs-keyword">return</span> NextResponse.json({ 
      answer: completion.choices[<span class="hljs-number">0</span>].message.content, 
      sources: results 
    });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>This route implements the RAG pattern. Here's how the complete RAG workflow works:</p>
<ol>
<li><p><strong>Converts the query to an embedding</strong>: The user's question is transformed into the same vector space as your document chunks. This uses the same embedding model (<code>text-embedding-3-small</code>) that processed the documents, ensuring they're in the same "vector space."</p>
</li>
<li><p><strong>Searches for similar chunks</strong>: Uses the <code>match_documents</code> function to find the 5 most semantically similar document chunks. This uses cosine similarity on the embeddings. Cosine similarity measures the angle between vectors - smaller angles mean more similar content, even if the exact words differ.</p>
</li>
<li><p><strong>Uses chunks as context</strong>: The retrieved chunks are passed to GPT-4o-mini as context. These chunks contain the most relevant information from your documents.</p>
</li>
<li><p><strong>Generates an answer</strong>: The AI model generates an answer based on the provided context. The system prompt instructs the AI to only answer based on the provided context, ensuring accuracy and preventing hallucinations.</p>
</li>
<li><p><strong>Returns results</strong>: Both the answer and source chunks are returned so users can verify the information.</p>
</li>
</ol>
<p>This RAG approach gives you several benefits. First, you get accuracy because answers are based on your actual documents, not just the AI's training data. Second, you get transparency because you can see which document chunks were used to generate each answer. Third, you get efficiency because only relevant chunks are used, which reduces token usage and costs. Finally, you get up-to-date information because you can update your knowledge base by uploading new documents without retraining the AI.</p>
<p>Now let's create the API route for managing documents.</p>
<h2 id="heading-step-7-create-the-documents-api-route">Step 7: Create the Documents API Route</h2>
<p>This route handles listing, viewing, downloading, and deleting documents. It serves multiple purposes depending on the query parameters.</p>
<p>Create <code>src/app/api/documents/route.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;

<span class="hljs-keyword">const</span> url = process.env.NEXT_PUBLIC_SUPABASE_URL!;
<span class="hljs-keyword">const</span> anonKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY!;
<span class="hljs-keyword">const</span> serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || anonKey;
<span class="hljs-keyword">const</span> supabase = createClient(url, anonKey);
<span class="hljs-keyword">const</span> supabaseStorage = createClient(url, serviceKey);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params">req: Request</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> reqUrl = <span class="hljs-keyword">new</span> URL(req.url);
    <span class="hljs-keyword">const</span> id = reqUrl.searchParams.get(<span class="hljs-string">'id'</span>);
    <span class="hljs-keyword">const</span> file = reqUrl.searchParams.get(<span class="hljs-string">'file'</span>) === <span class="hljs-string">'true'</span>;
    <span class="hljs-keyword">const</span> view = reqUrl.searchParams.get(<span class="hljs-string">'view'</span>) === <span class="hljs-string">'true'</span>;

    <span class="hljs-comment">// Handle file download/view</span>
    <span class="hljs-keyword">if</span> (id &amp;&amp; file) {
      <span class="hljs-keyword">const</span> { data: documents } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">'documents'</span>)
        .select(<span class="hljs-string">'metadata'</span>)
        .eq(<span class="hljs-string">'metadata-&gt;&gt;document_id'</span>, id)
        .limit(<span class="hljs-number">1</span>);

      <span class="hljs-keyword">if</span> (!documents || documents.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">'Document not found'</span> }, { status: <span class="hljs-number">404</span> });
      }

      <span class="hljs-keyword">const</span> meta = documents[<span class="hljs-number">0</span>].metadata;
      <span class="hljs-keyword">const</span> fileName = meta?.file_name || <span class="hljs-string">'document'</span>;
      <span class="hljs-keyword">const</span> fileType = meta?.file_type || <span class="hljs-string">'application/octet-stream'</span>;
      <span class="hljs-keyword">const</span> filePath = meta?.file_path || <span class="hljs-string">`<span class="hljs-subst">${id}</span>.<span class="hljs-subst">${fileName.split(<span class="hljs-string">'.'</span>).pop() || <span class="hljs-string">'pdf'</span>}</span>`</span>;

      <span class="hljs-keyword">const</span> { data: fileData, error: downloadError } = <span class="hljs-keyword">await</span> supabaseStorage.storage
        .from(<span class="hljs-string">'documents'</span>)
        .download(filePath);

      <span class="hljs-keyword">if</span> (downloadError || !fileData) {
        <span class="hljs-keyword">return</span> NextResponse.json({ 
          error: downloadError?.message || <span class="hljs-string">'File not stored'</span> 
        }, { status: <span class="hljs-number">404</span> });
      }

      <span class="hljs-keyword">const</span> buffer = Buffer.from(<span class="hljs-keyword">await</span> fileData.arrayBuffer());
      <span class="hljs-keyword">if</span> (buffer.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">'File is empty'</span> }, { status: <span class="hljs-number">500</span> });
      }

      <span class="hljs-keyword">const</span> isPDF = fileType === <span class="hljs-string">'application/pdf'</span> || fileName.toLowerCase().endsWith(<span class="hljs-string">'.pdf'</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> NextResponse(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Uint8Array</span>(buffer), {
        headers: {
          <span class="hljs-string">'Content-Type'</span>: fileType,
          <span class="hljs-string">'Content-Disposition'</span>: (view &amp;&amp; isPDF) 
            ? <span class="hljs-string">`inline; filename="<span class="hljs-subst">${fileName}</span>"`</span> 
            : <span class="hljs-string">`attachment; filename="<span class="hljs-subst">${fileName}</span>"`</span>,
          <span class="hljs-string">'Content-Length'</span>: buffer.length.toString(),
          ...(view &amp;&amp; isPDF ? { <span class="hljs-string">'X-Content-Type-Options'</span>: <span class="hljs-string">'nosniff'</span> } : {}),
        },
      });
    }

    <span class="hljs-comment">// Get single document with text content</span>
    <span class="hljs-keyword">if</span> (id) {
      <span class="hljs-keyword">const</span> { data: chunks, error } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">'documents'</span>)
        .select(<span class="hljs-string">'content, metadata'</span>)
        .eq(<span class="hljs-string">'metadata-&gt;&gt;document_id'</span>, id)
        .order(<span class="hljs-string">'metadata-&gt;&gt;chunk_index'</span>, { ascending: <span class="hljs-literal">true</span> });

      <span class="hljs-keyword">if</span> (error || !chunks || chunks.length === <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">'Document not found'</span> }, { status: <span class="hljs-number">404</span> });
      }

      <span class="hljs-keyword">const</span> m = chunks[<span class="hljs-number">0</span>].metadata || {};
      <span class="hljs-keyword">return</span> NextResponse.json({
        id,
        file_name: m.file_name || <span class="hljs-string">'Unknown'</span>,
        file_type: m.file_type || <span class="hljs-string">'unknown'</span>,
        file_size: m.file_size || <span class="hljs-number">0</span>,
        upload_date: m.upload_date || <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(),
        total_chunks: chunks.length,
        fullText: chunks.map(<span class="hljs-function">(<span class="hljs-params">c: <span class="hljs-built_in">any</span></span>) =&gt;</span> c.content).join(<span class="hljs-string">'\n\n'</span>),
        file_url: m.file_url,
        file_path: m.file_path
      });
    }

    <span class="hljs-comment">// List all documents</span>
    <span class="hljs-keyword">const</span> { data: documents, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'documents'</span>)
      .select(<span class="hljs-string">'metadata'</span>);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-comment">// Deduplicate documents by document_id</span>
    <span class="hljs-comment">// Since each document is split into multiple chunks, we need to group them</span>
    <span class="hljs-keyword">const</span> map = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>();
    documents?.forEach(<span class="hljs-function">(<span class="hljs-params">doc: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
      <span class="hljs-keyword">const</span> m = doc.metadata;
      <span class="hljs-keyword">if</span> (m?.document_id &amp;&amp; !map.has(m.document_id)) {
        map.set(m.document_id, {
          id: m.document_id,
          file_name: m.file_name || <span class="hljs-string">'Unknown'</span>,
          file_type: m.file_type || <span class="hljs-string">'unknown'</span>,
          file_size: m.file_size || <span class="hljs-number">0</span>,
          upload_date: m.upload_date || <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString(),
          total_chunks: m.total_chunks || <span class="hljs-number">0</span>,
          file_url: m.file_url,
          file_path: m.file_path,
        });
      }
    });

    <span class="hljs-keyword">return</span> NextResponse.json({ documents: <span class="hljs-built_in">Array</span>.from(map.values()) });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DELETE</span>(<span class="hljs-params">req: Request</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> id = <span class="hljs-keyword">new</span> URL(req.url).searchParams.get(<span class="hljs-string">'id'</span>);
    <span class="hljs-keyword">if</span> (!id) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">'Document ID required'</span> }, { status: <span class="hljs-number">400</span> });
    }

    <span class="hljs-comment">// Get file path from metadata</span>
    <span class="hljs-keyword">const</span> { data: docs } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'documents'</span>)
      .select(<span class="hljs-string">'metadata'</span>)
      .eq(<span class="hljs-string">'metadata-&gt;&gt;document_id'</span>, id)
      .limit(<span class="hljs-number">1</span>);

    <span class="hljs-keyword">const</span> filePath = docs?.[<span class="hljs-number">0</span>]?.metadata?.file_path;

    <span class="hljs-comment">// Delete file from storage</span>
    <span class="hljs-keyword">if</span> (filePath) {
      <span class="hljs-keyword">await</span> supabaseStorage.storage.from(<span class="hljs-string">'documents'</span>).remove([filePath]);
    }

    <span class="hljs-comment">// Delete all chunks from database</span>
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'documents'</span>)
      .delete()
      .eq(<span class="hljs-string">'metadata-&gt;&gt;document_id'</span>, id);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span>, fileDeleted: !!filePath });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>This route handles:</p>
<ul>
<li><p><strong>GET without ID</strong>: Lists all documents (deduplicated since each document has multiple chunks)</p>
</li>
<li><p><strong>GET with ID</strong>: Returns document details and full text (all chunks combined)</p>
</li>
<li><p><strong>GET with ID and file=true</strong>: Downloads the original file from storage</p>
</li>
<li><p><strong>DELETE with ID</strong>: Deletes the document and its file from both storage and database</p>
</li>
</ul>
<p>Now that your API routes are complete, let's build the user interface components, starting with the upload modal.</p>
<h2 id="heading-step-8-create-the-upload-modal-component">Step 8: Create the Upload Modal Component</h2>
<p>The upload modal provides a user-friendly interface for selecting and uploading documents. It handles file selection, upload progress, and displays success or error messages.</p>
<p>Create <code>src/app/components/UploadModal.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">interface</span> UploadModalProps {
  isOpen: <span class="hljs-built_in">boolean</span>;
  onClose: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
  onUploadSuccess?: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">UploadModal</span>(<span class="hljs-params">{ isOpen, onClose, onUploadSuccess }: UploadModalProps</span>) </span>{
  <span class="hljs-keyword">const</span> [file, setFile] = useState&lt;File | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [uploading, setUploading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [message, setMessage] = useState&lt;{ <span class="hljs-keyword">type</span>: <span class="hljs-string">'success'</span> | <span class="hljs-string">'error'</span>; text: <span class="hljs-built_in">string</span> } | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">document</span>.body.style.overflow = isOpen ? <span class="hljs-string">'hidden'</span> : <span class="hljs-string">'unset'</span>;
    <span class="hljs-keyword">if</span> (!isOpen) { 
      setFile(<span class="hljs-literal">null</span>); 
      setMessage(<span class="hljs-literal">null</span>); 
    }
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> { 
      <span class="hljs-built_in">document</span>.body.style.overflow = <span class="hljs-string">'unset'</span>; 
    };
  }, [isOpen]);

  <span class="hljs-keyword">const</span> handleFileChange = <span class="hljs-function">(<span class="hljs-params">e: React.ChangeEvent&lt;HTMLInputElement&gt;</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (e.target.files &amp;&amp; e.target.files[<span class="hljs-number">0</span>]) {
      setFile(e.target.files[<span class="hljs-number">0</span>]);
      setMessage(<span class="hljs-literal">null</span>);
    }
  };

  <span class="hljs-keyword">const</span> handleUpload = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!file) {
      setMessage({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'error'</span>, text: <span class="hljs-string">'Please select a file'</span> });
      <span class="hljs-keyword">return</span>;
    }

    setUploading(<span class="hljs-literal">true</span>);
    setMessage(<span class="hljs-literal">null</span>);

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData();
      formData.append(<span class="hljs-string">'file'</span>, file);

      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/upload'</span>, {
        method: <span class="hljs-string">'POST'</span>,
        body: formData,
      });

      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();

      <span class="hljs-keyword">if</span> (data.success) {
        setMessage({
          <span class="hljs-keyword">type</span>: <span class="hljs-string">'success'</span>,
          text: <span class="hljs-string">`File "<span class="hljs-subst">${data.fileName}</span>" uploaded successfully! Processed <span class="hljs-subst">${data.chunks}</span> chunks.`</span>,
        });
        setFile(<span class="hljs-literal">null</span>);
        (<span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">'upload-file-input'</span>) <span class="hljs-keyword">as</span> HTMLInputElement)?.setAttribute(<span class="hljs-string">'value'</span>, <span class="hljs-string">''</span>);
        <span class="hljs-built_in">setTimeout</span>(<span class="hljs-function">() =&gt;</span> { 
          onUploadSuccess?.(); 
          onClose(); 
        }, <span class="hljs-number">1500</span>);
      } <span class="hljs-keyword">else</span> {
        setMessage({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'error'</span>, text: data.error || <span class="hljs-string">'Upload failed'</span> });
      }
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      setMessage({ <span class="hljs-keyword">type</span>: <span class="hljs-string">'error'</span>, text: error.message || <span class="hljs-string">'Upload failed'</span> });
    } <span class="hljs-keyword">finally</span> {
      setUploading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">if</span> (!isOpen) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">return</span> (
    &lt;div
      className=<span class="hljs-string">"fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"</span>
      onClick={onClose}
    &gt;
      &lt;div
        className=<span class="hljs-string">"relative bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto"</span>
        onClick={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.stopPropagation()}
      &gt;
        &lt;div className=<span class="hljs-string">"flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800"</span>&gt;
          &lt;h2 className=<span class="hljs-string">"text-2xl font-semibold text-gray-900 dark:text-gray-100"</span>&gt;
            Upload Document
          &lt;/h2&gt;
          &lt;button
            onClick={onClose}
            className=<span class="hljs-string">"p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"</span>
            aria-label=<span class="hljs-string">"Close"</span>
          &gt;
            &lt;svg className=<span class="hljs-string">"w-6 h-6"</span> fill=<span class="hljs-string">"none"</span> stroke=<span class="hljs-string">"currentColor"</span> viewBox=<span class="hljs-string">"0 0 24 24"</span>&gt;
              &lt;path strokeLinecap=<span class="hljs-string">"round"</span> strokeLinejoin=<span class="hljs-string">"round"</span> strokeWidth={<span class="hljs-number">2</span>} d=<span class="hljs-string">"M6 18L18 6M6 6l12 12"</span> /&gt;
            &lt;/svg&gt;
          &lt;/button&gt;
        &lt;/div&gt;

        &lt;div className=<span class="hljs-string">"p-6"</span>&gt;
          &lt;div className=<span class="hljs-string">"mb-6"</span>&gt;
            &lt;label htmlFor=<span class="hljs-string">"upload-file-input"</span> className=<span class="hljs-string">"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"</span>&gt;
              Select a file (PDF, DOCX, or TXT)
            &lt;/label&gt;
            &lt;input
              id=<span class="hljs-string">"upload-file-input"</span>
              <span class="hljs-keyword">type</span>=<span class="hljs-string">"file"</span>
              accept=<span class="hljs-string">".pdf,.docx,.txt"</span>
              onChange={handleFileChange}
              className=<span class="hljs-string">"block w-full text-sm text-gray-500
                file:mr-4 file:py-2 file:px-4
                file:rounded-lg file:border-0
                file:text-sm file:font-semibold
                file:bg-blue-50 file:text-blue-700
                hover:file:bg-blue-100
                dark:file:bg-blue-900 dark:file:text-blue-300
                dark:hover:file:bg-blue-800"</span>
            /&gt;
          &lt;/div&gt;

          {file &amp;&amp; (
            &lt;div className=<span class="hljs-string">"mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg text-sm text-gray-600 dark:text-gray-400 space-y-1"</span>&gt;
              &lt;p&gt;&lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Selected:&lt;<span class="hljs-regexp">/span&gt; {file.name}&lt;/</span>p&gt;
              &lt;p&gt;&lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Size:&lt;<span class="hljs-regexp">/span&gt; {(file.size /</span> <span class="hljs-number">1024</span>).toFixed(<span class="hljs-number">2</span>)} KB&lt;/p&gt;
              &lt;p&gt;&lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Type:&lt;<span class="hljs-regexp">/span&gt; {file.type || file.name.split('.').pop()}&lt;/</span>p&gt;
            &lt;/div&gt;
          )}

          &lt;button
            onClick={handleUpload}
            disabled={!file || uploading}
            className=<span class="hljs-string">"w-full bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium"</span>
          &gt;
            {uploading ? <span class="hljs-string">'Uploading and Processing...'</span> : <span class="hljs-string">'Upload Document'</span>}
          &lt;/button&gt;

          {message &amp;&amp; (
            &lt;div
              className={<span class="hljs-string">`mt-6 p-4 rounded-lg <span class="hljs-subst">${
                message.<span class="hljs-keyword">type</span> === <span class="hljs-string">'success'</span>
                  ? <span class="hljs-string">'bg-green-50 text-green-800 dark:bg-green-900 dark:text-green-200'</span>
                  : <span class="hljs-string">'bg-red-50 text-red-800 dark:bg-red-900 dark:text-red-200'</span>
              }</span>`</span>}
            &gt;
              {message.text}
            &lt;/div&gt;
          )}

          &lt;div className=<span class="hljs-string">"mt-8 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg text-sm"</span>&gt;
            &lt;p className=<span class="hljs-string">"font-medium text-blue-900 dark:text-blue-200 mb-2"</span>&gt;Supported: PDF, DOCX, TXT&lt;/p&gt;
            &lt;p className=<span class="hljs-string">"text-blue-700 dark:text-blue-400"</span>&gt;Files will be processed and embedded <span class="hljs-keyword">for</span> RAG search.&lt;/p&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This component provides a clean interface for file uploads with proper error handling and user feedback. Next, let's create the PDF viewer component for previewing documents.</p>
<h2 id="heading-step-9-create-the-pdf-viewer-modal-component">Step 9: Create the PDF Viewer Modal Component</h2>
<p>The PDF viewer modal allows users to preview PDFs and view extracted text from any document. It's particularly useful for verifying that documents were processed correctly.</p>
<p>Create <code>src/app/components/PDFViewerModal.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">interface</span> PDFViewerModalProps {
  isOpen: <span class="hljs-built_in">boolean</span>;
  onClose: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
  fileUrl: <span class="hljs-built_in">string</span>;
  fileName: <span class="hljs-built_in">string</span>;
  documentId?: <span class="hljs-built_in">string</span>;
  isPDF?: <span class="hljs-built_in">boolean</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">PDFViewerModal</span>(<span class="hljs-params">{ 
  isOpen, 
  onClose, 
  fileUrl, 
  fileName, 
  documentId, 
  isPDF = <span class="hljs-literal">true</span> 
}: PDFViewerModalProps</span>) </span>{
  <span class="hljs-keyword">const</span> [error, setError] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [activeTab, setActiveTab] = useState&lt;<span class="hljs-string">'preview'</span> | <span class="hljs-string">'content'</span>&gt;(<span class="hljs-string">'preview'</span>);
  <span class="hljs-keyword">const</span> [text, setText] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [textLoading, setTextLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [textError, setTextError] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">document</span>.body.style.overflow = isOpen ? <span class="hljs-string">'hidden'</span> : <span class="hljs-string">'unset'</span>;
    <span class="hljs-keyword">if</span> (isOpen) { 
      setError(<span class="hljs-literal">null</span>); 
      setLoading(<span class="hljs-literal">true</span>); 
      setActiveTab(isPDF ? <span class="hljs-string">'preview'</span> : <span class="hljs-string">'content'</span>); 
      setText(<span class="hljs-string">''</span>); 
      setTextError(<span class="hljs-literal">null</span>); 
    }
    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> { 
      <span class="hljs-built_in">document</span>.body.style.overflow = <span class="hljs-string">'unset'</span>; 
    };
  }, [isOpen, isPDF]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (isOpen &amp;&amp; documentId &amp;&amp; activeTab === <span class="hljs-string">'content'</span> &amp;&amp; !text &amp;&amp; !textLoading &amp;&amp; !textError) {
      fetchDocumentText();
    }
  }, [isOpen, documentId, activeTab, text, textLoading, textError]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (isOpen &amp;&amp; fileUrl &amp;&amp; isPDF) {
      fetch(fileUrl, { method: <span class="hljs-string">'GET'</span>, headers: { <span class="hljs-string">'Accept'</span>: <span class="hljs-string">'application/json'</span> } })
        .then(<span class="hljs-keyword">async</span> res =&gt; {
          <span class="hljs-keyword">if</span> (res.headers.get(<span class="hljs-string">'content-type'</span>)?.includes(<span class="hljs-string">'application/json'</span>)) {
            <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
            <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(data.error || <span class="hljs-string">'File not available'</span>);
          }
          <span class="hljs-keyword">if</span> (!res.ok) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Failed to load: <span class="hljs-subst">${res.status}</span>`</span>);
          setLoading(<span class="hljs-literal">false</span>);
        })
        .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
          setError(err.message || <span class="hljs-string">'Failed to load PDF'</span>);
          setLoading(<span class="hljs-literal">false</span>);
        });
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (isOpen &amp;&amp; !isPDF) {
      setLoading(<span class="hljs-literal">false</span>);
    }
  }, [isOpen, fileUrl, isPDF]);

  <span class="hljs-keyword">const</span> fetchDocumentText = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!documentId) <span class="hljs-keyword">return</span>;
    setTextLoading(<span class="hljs-literal">true</span>); 
    setTextError(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/documents?id=<span class="hljs-subst">${documentId}</span>`</span>);
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
      <span class="hljs-keyword">if</span> (data.error) {
        setTextError(data.error);
      } <span class="hljs-keyword">else</span> {
        setText(data.fullText || <span class="hljs-string">'No text content available'</span>);
      }
    } <span class="hljs-keyword">catch</span> (err) {
      setTextError(err <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? err.message : <span class="hljs-string">'Failed to fetch document text'</span>);
    } <span class="hljs-keyword">finally</span> {
      setTextLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">if</span> (!isOpen) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">return</span> (
    &lt;div
      className=<span class="hljs-string">"fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75 p-4"</span>
      onClick={onClose}
    &gt;
      &lt;div
        className=<span class="hljs-string">"relative bg-white dark:bg-gray-900 rounded-lg shadow-xl w-full max-w-6xl h-[90vh] flex flex-col"</span>
        onClick={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> e.stopPropagation()}
      &gt;
        &lt;div className=<span class="hljs-string">"flex flex-col border-b border-gray-200 dark:border-gray-800"</span>&gt;
          &lt;div className=<span class="hljs-string">"flex items-center justify-between p-4"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-xl font-semibold text-gray-900 dark:text-gray-100 truncate flex-1 mr-4"</span>&gt;
              {fileName}
            &lt;/h2&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-2"</span>&gt;
              &lt;button
                onClick={onClose}
                className=<span class="hljs-string">"p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800"</span>
                aria-label=<span class="hljs-string">"Close"</span>
              &gt;
                &lt;svg className=<span class="hljs-string">"w-6 h-6"</span> fill=<span class="hljs-string">"none"</span> stroke=<span class="hljs-string">"currentColor"</span> viewBox=<span class="hljs-string">"0 0 24 24"</span>&gt;
                  &lt;path strokeLinecap=<span class="hljs-string">"round"</span> strokeLinejoin=<span class="hljs-string">"round"</span> strokeWidth={<span class="hljs-number">2</span>} d=<span class="hljs-string">"M6 18L18 6M6 6l12 12"</span> /&gt;
                &lt;/svg&gt;
              &lt;/button&gt;
            &lt;/div&gt;
          &lt;/div&gt;

          {isPDF &amp;&amp; (
            &lt;div className=<span class="hljs-string">"flex border-t border-gray-200 dark:border-gray-800"</span>&gt;
              {([<span class="hljs-string">'preview'</span>, <span class="hljs-string">'content'</span>] <span class="hljs-keyword">as</span> <span class="hljs-keyword">const</span>).map(<span class="hljs-function"><span class="hljs-params">tab</span> =&gt;</span> (
                &lt;button 
                  key={tab} 
                  onClick={<span class="hljs-function">() =&gt;</span> setActiveTab(tab)} 
                  className={<span class="hljs-string">`flex-1 px-4 py-3 text-sm font-medium transition-colors <span class="hljs-subst">${
                    activeTab === tab 
                      ? <span class="hljs-string">'text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400 bg-blue-50 dark:bg-blue-900/20'</span> 
                      : <span class="hljs-string">'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800'</span>
                  }</span>`</span>}
                &gt;
                  {tab.charAt(<span class="hljs-number">0</span>).toUpperCase() + tab.slice(<span class="hljs-number">1</span>)}
                &lt;/button&gt;
              ))}
            &lt;/div&gt;
          )}
        &lt;/div&gt;

        &lt;div className=<span class="hljs-string">"flex-1 overflow-hidden"</span>&gt;
          {isPDF &amp;&amp; activeTab === <span class="hljs-string">'preview'</span> &amp;&amp; (
            &lt;div className=<span class="hljs-string">"h-full overflow-hidden"</span>&gt;
              {error ? (
                &lt;div className=<span class="hljs-string">"flex flex-col items-center justify-center h-full p-8"</span>&gt;
                  &lt;div className=<span class="hljs-string">"bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-6 max-w-md"</span>&gt;
                    &lt;h3 className=<span class="hljs-string">"text-lg font-semibold text-yellow-800 dark:text-yellow-200 mb-2"</span>&gt;
                      PDF File Not Available
                    &lt;/h3&gt;
                    &lt;p className=<span class="hljs-string">"text-yellow-700 dark:text-yellow-300 mb-4"</span>&gt;{error}&lt;/p&gt;
                    {documentId &amp;&amp; (
                      &lt;button 
                        onClick={<span class="hljs-function">() =&gt;</span> setActiveTab(<span class="hljs-string">'content'</span>)} 
                        className=<span class="hljs-string">"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"</span>
                      &gt;
                        View Extracted Text Instead
                      &lt;/button&gt;
                    )}
                  &lt;/div&gt;
                &lt;/div&gt;
              ) : loading ? (
                &lt;div className=<span class="hljs-string">"flex items-center justify-center h-full"</span>&gt;
                  &lt;p className=<span class="hljs-string">"text-gray-500 dark:text-gray-400"</span>&gt;Loading PDF...&lt;/p&gt;
                &lt;/div&gt;
              ) : (
                &lt;iframe
                  src={<span class="hljs-string">`<span class="hljs-subst">${fileUrl}</span><span class="hljs-subst">${fileUrl.includes(<span class="hljs-string">'?'</span>) ? <span class="hljs-string">'&amp;'</span> : <span class="hljs-string">'?'</span>}</span>view=true#toolbar=1&amp;navpanes=0&amp;scrollbar=1`</span>}
                  className=<span class="hljs-string">"w-full h-full border-0"</span>
                  title={fileName}
                  allow=<span class="hljs-string">"fullscreen"</span>
                  onError={<span class="hljs-function">() =&gt;</span> setError(<span class="hljs-string">'Failed to load PDF'</span>)}
                /&gt;
              )}
            &lt;/div&gt;
          )}

          {(!isPDF || activeTab === <span class="hljs-string">'content'</span>) &amp;&amp; (
            &lt;div className=<span class="hljs-string">"h-full overflow-auto p-6"</span>&gt;
              {textLoading ? (
                &lt;div className=<span class="hljs-string">"flex items-center justify-center h-full"</span>&gt;
                  &lt;p className=<span class="hljs-string">"text-gray-500 dark:text-gray-400"</span>&gt;Loading...&lt;/p&gt;
                &lt;/div&gt;
              ) : textError ? (
                &lt;div className=<span class="hljs-string">"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"</span>&gt;
                  &lt;p className=<span class="hljs-string">"text-red-800 dark:text-red-200"</span>&gt;<span class="hljs-built_in">Error</span>: {textError}&lt;/p&gt;
                &lt;/div&gt;
              ) : (
                &lt;div className=<span class="hljs-string">"space-y-4"</span>&gt;
                  &lt;p className=<span class="hljs-string">"text-sm text-gray-500 dark:text-gray-400"</span>&gt;
                    Formatting may be inconsistent <span class="hljs-keyword">from</span> source.
                  &lt;/p&gt;
                  &lt;pre className=<span class="hljs-string">"whitespace-pre-wrap text-sm text-gray-800 dark:text-gray-200 font-mono bg-gray-50 dark:bg-gray-800 p-4 rounded-lg"</span>&gt;
                    {text || <span class="hljs-string">'No text content available'</span>}
                  &lt;/pre&gt;
                &lt;/div&gt;
              )}
            &lt;/div&gt;
          )}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This component provides a full-screen modal for viewing PDFs and extracted text, with tabs to switch between preview and text content. Now let's create a simple navigation component to tie everything together.</p>
<h2 id="heading-step-10-create-the-navigation-component">Step 10: Create the Navigation Component</h2>
<p>The navigation component provides easy access to the Search and Documents pages. It highlights the current page and provides a clean, consistent navigation experience.</p>
<p>Create <code>src/app/components/Navigation.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> Link <span class="hljs-keyword">from</span> <span class="hljs-string">'next/link'</span>;
<span class="hljs-keyword">import</span> { usePathname } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/navigation'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Navigation</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> pathname = usePathname();

  <span class="hljs-keyword">const</span> navItems = [
    { href: <span class="hljs-string">'/'</span>, label: <span class="hljs-string">'Search'</span> },
    { href: <span class="hljs-string">'/documents'</span>, label: <span class="hljs-string">'Documents'</span> },
  ];

  <span class="hljs-keyword">return</span> (
    &lt;nav className=<span class="hljs-string">"border-b border-gray-200 dark:border-gray-800 mb-8"</span>&gt;
      &lt;div className=<span class="hljs-string">"max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex space-x-8"</span>&gt;
          {navItems.map(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> (
            &lt;Link
              key={item.href}
              href={item.href}
              className={<span class="hljs-string">`py-4 px-1 border-b-2 font-medium text-sm <span class="hljs-subst">${
                pathname === item.href
                  ? <span class="hljs-string">'border-blue-500 text-blue-600 dark:text-blue-400'</span>
                  : <span class="hljs-string">'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'</span>
              }</span>`</span>}
            &gt;
              {item.label}
            &lt;/Link&gt;
          ))}
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/nav&gt;
  );
}
</code></pre>
<p>With navigation in place, let's create the main search page where users can query their documents.</p>
<h2 id="heading-step-11-create-the-home-page-search-interface">Step 11: Create the Home Page (Search Interface)</h2>
<p>The search page is the main interface where users ask questions about their uploaded documents. It displays the AI-generated answers along with source citations, allowing users to verify the information.</p>
<p>Update <code>src/app/page.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> Navigation <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/Navigation'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">Home</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [query, setQuery] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [answer, setAnswer] = useState(<span class="hljs-string">''</span>);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [sources, setSources] = useState&lt;<span class="hljs-built_in">any</span>[]&gt;([]);

  <span class="hljs-keyword">const</span> handleSearch = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!query.trim()) <span class="hljs-keyword">return</span>;
    setLoading(<span class="hljs-literal">true</span>); 
    setAnswer(<span class="hljs-string">''</span>); 
    setSources([]);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/search'</span>, { 
        method: <span class="hljs-string">'POST'</span>, 
        headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> }, 
        body: <span class="hljs-built_in">JSON</span>.stringify({ query }) 
      });
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
      <span class="hljs-keyword">if</span> (data.error) {
        setAnswer(<span class="hljs-string">`Error: <span class="hljs-subst">${data.error}</span>`</span>);
      } <span class="hljs-keyword">else</span> { 
        setAnswer(data.answer || <span class="hljs-string">'No answer generated'</span>); 
        setSources(data.sources || []); 
      }
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      setAnswer(<span class="hljs-string">`Error: <span class="hljs-subst">${error.message}</span>`</span>);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> handleKeyPress = <span class="hljs-function">(<span class="hljs-params">e: React.KeyboardEvent</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (e.key === <span class="hljs-string">'Enter'</span> &amp;&amp; (e.metaKey || e.ctrlKey)) {
      handleSearch();
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"min-h-screen"</span>&gt;
      &lt;Navigation /&gt;
      &lt;main className=<span class="hljs-string">"max-w-4xl mx-auto p-8"</span>&gt;
        &lt;h1 className=<span class="hljs-string">"text-3xl font-bold mb-6"</span>&gt;RAG Search&lt;/h1&gt;

        &lt;div className=<span class="hljs-string">"bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6 shadow-sm mb-6"</span>&gt;
          &lt;textarea 
            className=<span class="hljs-string">"w-full p-4 border border-gray-300 dark:border-gray-700 rounded-lg shadow-sm bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"</span>
            placeholder=<span class="hljs-string">"Ask a question about your uploaded documents..."</span>
            value={query}
            onChange={<span class="hljs-function">(<span class="hljs-params">e</span>) =&gt;</span> setQuery(e.target.value)}
            onKeyDown={handleKeyPress}
            rows={<span class="hljs-number">4</span>}
          /&gt;
          &lt;button 
            onClick={handleSearch}
            className=<span class="hljs-string">"mt-4 bg-blue-600 text-white px-8 py-3 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed font-medium"</span>
            disabled={loading || !query.trim()}
          &gt;
            {loading ? <span class="hljs-string">'Searching...'</span> : <span class="hljs-string">'Search'</span>}
          &lt;/button&gt;
          &lt;p className=<span class="hljs-string">"mt-2 text-sm text-gray-500 dark:text-gray-400"</span>&gt;
            Press Cmd/Ctrl + Enter to search
          &lt;/p&gt;
        &lt;/div&gt;

        {answer &amp;&amp; (
          &lt;div className=<span class="hljs-string">"bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6 shadow-sm mb-6"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-xl font-semibold mb-3"</span>&gt;Answer:&lt;/h2&gt;
            &lt;p className=<span class="hljs-string">"text-gray-800 dark:text-gray-200 leading-relaxed whitespace-pre-wrap"</span>&gt;
              {answer}
            &lt;/p&gt;
          &lt;/div&gt;
        )}

        {sources &amp;&amp; sources.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
          &lt;div className=<span class="hljs-string">"bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg p-6 shadow-sm"</span>&gt;
            &lt;h2 className=<span class="hljs-string">"text-xl font-semibold mb-3"</span>&gt;Sources ({sources.length}):&lt;/h2&gt;
            &lt;div className=<span class="hljs-string">"space-y-3"</span>&gt;
              {sources.map(<span class="hljs-function">(<span class="hljs-params">source, index</span>) =&gt;</span> (
                &lt;div
                  key={index}
                  className=<span class="hljs-string">"p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700"</span>
                &gt;
                  &lt;p className=<span class="hljs-string">"text-sm text-gray-600 dark:text-gray-400 mb-1"</span>&gt;
                    &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Source:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                    {source.metadata?.source || source.metadata?.file_name || <span class="hljs-string">'Unknown'</span>}
                  &lt;/p&gt;
                  &lt;p className=<span class="hljs-string">"text-sm text-gray-800 dark:text-gray-200 line-clamp-3"</span>&gt;
                    {source.content}
                  &lt;/p&gt;
                &lt;/div&gt;
              ))}
            &lt;/div&gt;
          &lt;/div&gt;
        )}
      &lt;/main&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This page provides a clean search interface with a textarea for queries, a search button, and sections to display answers and source citations. The sources section helps users verify where the information came from, which is crucial for trust and accuracy. Now let's create the documents management page.</p>
<h2 id="heading-step-12-create-the-documents-page">Step 12: Create the Documents Page</h2>
<p>The documents page serves as your document library. It displays all uploaded documents in a table format, shows metadata like file size and chunk count, and provides actions to preview, download, or delete documents. This page is essential for managing your document collection and verifying uploads.</p>
<p>Create <code>src/app/documents/page.tsx</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;
<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> Navigation <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/Navigation'</span>;
<span class="hljs-keyword">import</span> PDFViewerModal <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/PDFViewerModal'</span>;
<span class="hljs-keyword">import</span> UploadModal <span class="hljs-keyword">from</span> <span class="hljs-string">'../components/UploadModal'</span>;

<span class="hljs-keyword">interface</span> Document {
  id: <span class="hljs-built_in">string</span>;
  file_name: <span class="hljs-built_in">string</span>;
  file_type: <span class="hljs-built_in">string</span>;
  file_size: <span class="hljs-built_in">number</span>;
  upload_date: <span class="hljs-built_in">string</span>;
  total_chunks: <span class="hljs-built_in">number</span>;
  file_url?: <span class="hljs-built_in">string</span>;
  file_path?: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">DocumentsPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [documents, setDocuments] = useState&lt;Document[]&gt;([]);
  <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">const</span> [error, setError] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [showPDFModal, setShowPDFModal] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> [selectedPDF, setSelectedPDF] = useState&lt;{ url: <span class="hljs-built_in">string</span>; name: <span class="hljs-built_in">string</span>; id?: <span class="hljs-built_in">string</span>; isPDF?: <span class="hljs-built_in">boolean</span> } | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [deletingId, setDeletingId] = useState&lt;<span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
  <span class="hljs-keyword">const</span> [showUploadModal, setShowUploadModal] = useState(<span class="hljs-literal">false</span>);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetchDocuments();
  }, []);

  <span class="hljs-keyword">const</span> fetchDocuments = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      setLoading(<span class="hljs-literal">true</span>);
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/documents'</span>);
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
      <span class="hljs-keyword">if</span> (data.error) {
        setError(data.error);
      } <span class="hljs-keyword">else</span> {
        setDocuments(data.documents || []);
      }
    } <span class="hljs-keyword">catch</span> (err) {
      setError(err <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? err.message : <span class="hljs-string">'Failed to fetch documents'</span>);
    } <span class="hljs-keyword">finally</span> {
      setLoading(<span class="hljs-literal">false</span>);
    }
  };

  <span class="hljs-keyword">const</span> formatDate = <span class="hljs-function">(<span class="hljs-params">s: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> d = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>(s);
      <span class="hljs-keyword">return</span> <span class="hljs-built_in">isNaN</span>(d.getTime()) 
        ? s 
        : d.toLocaleString(<span class="hljs-string">'en-US'</span>, { 
            year: <span class="hljs-string">'numeric'</span>, 
            month: <span class="hljs-string">'short'</span>, 
            day: <span class="hljs-string">'numeric'</span>, 
            hour: <span class="hljs-string">'2-digit'</span>, 
            minute: <span class="hljs-string">'2-digit'</span>, 
            hour12: <span class="hljs-literal">true</span> 
          });
    } <span class="hljs-keyword">catch</span> { 
      <span class="hljs-keyword">return</span> s; 
    }
  };

  <span class="hljs-keyword">const</span> formatFileSize = <span class="hljs-function">(<span class="hljs-params">b: <span class="hljs-built_in">number</span></span>) =&gt;</span> 
    b &lt; <span class="hljs-number">1024</span> 
      ? <span class="hljs-string">`<span class="hljs-subst">${b}</span> B`</span> 
      : b &lt; <span class="hljs-number">1024</span> * <span class="hljs-number">1024</span> 
        ? <span class="hljs-string">`<span class="hljs-subst">${(b / <span class="hljs-number">1024</span>).toFixed(<span class="hljs-number">2</span>)}</span> KB`</span> 
        : <span class="hljs-string">`<span class="hljs-subst">${(b / (<span class="hljs-number">1024</span> * <span class="hljs-number">1024</span>)).toFixed(<span class="hljs-number">2</span>)}</span> MB`</span>;

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-keyword">async</span> (id: <span class="hljs-built_in">string</span>, name: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">if</span> (!confirm(<span class="hljs-string">`Delete "<span class="hljs-subst">${name}</span>"? This will permanently delete the document, embeddings, and file.`</span>)) {
      <span class="hljs-keyword">return</span>;
    }
    setDeletingId(id);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/documents?id=<span class="hljs-subst">${id}</span>`</span>, { method: <span class="hljs-string">'DELETE'</span> });
      <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> res.json();
      <span class="hljs-keyword">if</span> (data.error) {
        alert(<span class="hljs-string">`Error: <span class="hljs-subst">${data.error}</span>`</span>);
      } <span class="hljs-keyword">else</span> {
        setDocuments(documents.filter(<span class="hljs-function"><span class="hljs-params">doc</span> =&gt;</span> doc.id !== id));
      }
    } <span class="hljs-keyword">catch</span> (err) {
      alert(err <span class="hljs-keyword">instanceof</span> <span class="hljs-built_in">Error</span> ? err.message : <span class="hljs-string">'Failed to delete'</span>);
    } <span class="hljs-keyword">finally</span> {
      setDeletingId(<span class="hljs-literal">null</span>);
    }
  };

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">"min-h-screen"</span>&gt;
      &lt;Navigation /&gt;
      &lt;main className=<span class="hljs-string">"max-w-7xl mx-auto p-8"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-center justify-between mb-6"</span>&gt;
          &lt;h1 className=<span class="hljs-string">"text-3xl font-bold"</span>&gt;Documents&lt;/h1&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> setShowUploadModal(<span class="hljs-literal">true</span>)}
            className=<span class="hljs-string">"px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium"</span>
          &gt;
            Upload Document
          &lt;/button&gt;
        &lt;/div&gt;

        {loading ? (
          &lt;div className=<span class="hljs-string">"text-center py-12"</span>&gt;
            &lt;p className=<span class="hljs-string">"text-gray-500 dark:text-gray-400"</span>&gt;Loading documents...&lt;/p&gt;
          &lt;/div&gt;
        ) : error ? (
          &lt;div className=<span class="hljs-string">"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4"</span>&gt;
            &lt;p className=<span class="hljs-string">"text-red-800 dark:text-red-200"</span>&gt;<span class="hljs-built_in">Error</span>: {error}&lt;/p&gt;
          &lt;/div&gt;
        ) : documents.length === <span class="hljs-number">0</span> ? (
          &lt;div className=<span class="hljs-string">"bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-12 text-center"</span>&gt;
            &lt;p className=<span class="hljs-string">"text-gray-500 dark:text-gray-400 mb-4"</span>&gt;No documents uploaded yet.&lt;/p&gt;
            &lt;button
              onClick={<span class="hljs-function">() =&gt;</span> setShowUploadModal(<span class="hljs-literal">true</span>)}
              className=<span class="hljs-string">"text-blue-600 dark:text-blue-400 hover:underline font-medium"</span>
            &gt;
              Upload your first <span class="hljs-built_in">document</span>
            &lt;/button&gt;
          &lt;/div&gt;
        ) : (
          &lt;div className=<span class="hljs-string">"bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-lg shadow-sm overflow-hidden"</span>&gt;
            &lt;div className=<span class="hljs-string">"overflow-x-auto"</span>&gt;
              &lt;table className=<span class="hljs-string">"min-w-full divide-y divide-gray-200 dark:divide-gray-800"</span>&gt;
                &lt;thead className=<span class="hljs-string">"bg-gray-50 dark:bg-gray-800"</span>&gt;
                  &lt;tr&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      File Name
                    &lt;/th&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      Type
                    &lt;/th&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      Size
                    &lt;/th&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      Chunks
                    &lt;/th&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      Upload <span class="hljs-built_in">Date</span>
                    &lt;/th&gt;
                    &lt;th className=<span class="hljs-string">"px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"</span>&gt;
                      Actions
                    &lt;/th&gt;
                  &lt;/tr&gt;
                &lt;/thead&gt;
                &lt;tbody className=<span class="hljs-string">"bg-white dark:bg-gray-900 divide-y divide-gray-200 dark:divide-gray-800"</span>&gt;
                  {documents.map(<span class="hljs-function">(<span class="hljs-params">doc</span>) =&gt;</span> (
                    &lt;tr key={doc.id} className=<span class="hljs-string">"hover:bg-gray-50 dark:hover:bg-gray-800"</span>&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap"</span>&gt;
                        &lt;div className=<span class="hljs-string">"text-sm font-medium text-gray-900 dark:text-gray-100"</span>&gt;
                          {doc.file_name}
                        &lt;/div&gt;
                      &lt;/td&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap"</span>&gt;
                        &lt;span className=<span class="hljs-string">"px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"</span>&gt;
                          {doc.file_type || <span class="hljs-string">'unknown'</span>}
                        &lt;/span&gt;
                      &lt;/td&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"</span>&gt;
                        {formatFileSize(doc.file_size)}
                      &lt;/td&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"</span>&gt;
                        {doc.total_chunks}
                      &lt;/td&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"</span>&gt;
                        {formatDate(doc.upload_date)}
                      &lt;/td&gt;
                      &lt;td className=<span class="hljs-string">"px-6 py-4 whitespace-nowrap text-sm font-medium"</span>&gt;
                        &lt;div className=<span class="hljs-string">"flex gap-3 items-center"</span>&gt;
                          {doc.file_name.toLowerCase().endsWith(<span class="hljs-string">'.pdf'</span>) ? (
                            &lt;button 
                              onClick={<span class="hljs-function">() =&gt;</span> {
                                <span class="hljs-keyword">const</span> pdfUrl = doc.file_url 
                                  ? <span class="hljs-string">`<span class="hljs-subst">${doc.file_url}</span>?view=true`</span> 
                                  : <span class="hljs-string">`/api/documents?id=<span class="hljs-subst">${doc.id}</span>&amp;file=true&amp;view=true`</span>;
                                setSelectedPDF({ url: pdfUrl, name: doc.file_name, id: doc.id });
                                setShowPDFModal(<span class="hljs-literal">true</span>);
                              }} 
                              className=<span class="hljs-string">"text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"</span>
                            &gt;
                              Preview
                            &lt;/button&gt;
                          ) : (
                            &lt;&gt;
                              &lt;button 
                                onClick={<span class="hljs-function">() =&gt;</span> {
                                  setSelectedPDF({ 
                                    url: doc.file_url || <span class="hljs-string">`/api/documents?id=<span class="hljs-subst">${doc.id}</span>&amp;file=true`</span>, 
                                    name: doc.file_name, 
                                    id: doc.id, 
                                    isPDF: <span class="hljs-literal">false</span> 
                                  });
                                  setShowPDFModal(<span class="hljs-literal">true</span>);
                                }} 
                                className=<span class="hljs-string">"text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300"</span>
                              &gt;
                                View
                              &lt;/button&gt;
                              {(doc.file_url || doc.file_path) &amp;&amp; (
                                &lt;a 
                                  href={doc.file_url || <span class="hljs-string">`/api/documents?id=<span class="hljs-subst">${doc.id}</span>&amp;file=true`</span>} 
                                  download={doc.file_name}
                                  className=<span class="hljs-string">"text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"</span> 
                                  target=<span class="hljs-string">"_blank"</span> 
                                  rel=<span class="hljs-string">"noopener noreferrer"</span>
                                &gt;
                                  Download
                                &lt;/a&gt;
                              )}
                            &lt;/&gt;
                          )}
                          &lt;button 
                            onClick={<span class="hljs-function">() =&gt;</span> handleDelete(doc.id, doc.file_name)} 
                            disabled={deletingId === doc.id}
                            className=<span class="hljs-string">"text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 disabled:opacity-50 disabled:cursor-not-allowed"</span>
                          &gt;
                            {deletingId === doc.id ? <span class="hljs-string">'Deleting...'</span> : <span class="hljs-string">'Delete'</span>}
                          &lt;/button&gt;
                        &lt;/div&gt;
                      &lt;/td&gt;
                    &lt;/tr&gt;
                  ))}
                &lt;/tbody&gt;
              &lt;/table&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        )}

        {selectedPDF &amp;&amp; (
          &lt;PDFViewerModal 
            isOpen={showPDFModal} 
            onClose={<span class="hljs-function">() =&gt;</span> { 
              setShowPDFModal(<span class="hljs-literal">false</span>); 
              setSelectedPDF(<span class="hljs-literal">null</span>); 
            }}
            fileUrl={selectedPDF.url} 
            fileName={selectedPDF.name} 
            documentId={selectedPDF.id} 
            isPDF={selectedPDF.isPDF !== <span class="hljs-literal">false</span>} 
          /&gt;
        )}
        &lt;UploadModal 
          isOpen={showUploadModal} 
          onClose={<span class="hljs-function">() =&gt;</span> setShowUploadModal(<span class="hljs-literal">false</span>)} 
          onUploadSuccess={fetchDocuments} 
        /&gt;
      &lt;/main&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>This page provides a comprehensive document management interface with a table showing all documents, their metadata, and action buttons for preview, download, and deletion. The page automatically refreshes after uploads and handles loading and error states gracefully.</p>
<p>Now that all your components and pages are built, let's test the complete application.</p>
<h2 id="heading-step-13-test-your-application">Step 13: Test Your Application</h2>
<p>Start your development server:</p>
<pre><code class="lang-typescript">npm run dev
</code></pre>
<p>Open <a target="_blank" href="http://localhost:3000/"><strong>http://localhost:3000</strong></a> in your browser.</p>
<h3 id="heading-test-the-upload-flow">Test the Upload Flow</h3>
<ol>
<li><p>Navigate to the Documents page</p>
</li>
<li><p>Click "Upload Document"</p>
</li>
<li><p>Select a PDF, DOCX, or TXT file</p>
</li>
<li><p>Wait for the upload and processing to complete (this may take a moment as embeddings are generated)</p>
</li>
<li><p>You should see your document in the list with its metadata:</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769376932518/cf1bcd3c-3ab2-4602-8df0-bca909c0edb0.png" alt="RAG search documents management page showing a table with uploaded documents." class="image--center mx-auto" width="2026" height="1296" loading="lazy"></p>
<h3 id="heading-test-the-search-flow">Test the Search Flow</h3>
<ol>
<li><p>Navigate to the Search page (or click "Search" in the navigation)</p>
</li>
<li><p>Make sure you've uploaded at least one document first</p>
</li>
<li><p>Type a question about your uploaded document (for example, "What is this document about?" or ask about specific content)</p>
</li>
<li><p>Click "Search" or press Cmd/Ctrl + Enter</p>
</li>
<li><p>You should see an AI-generated answer with source citations showing which document chunks were used</p>
</li>
</ol>
<p>Once the embedding is done, you can navigate to search and look for the sample test command based on the documents you have uploaded. You can also check the source from which the search results were pulled.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769377080953/c15678d6-59d0-4e97-8a1b-fe049e5fa6a9.png" alt="RAG Search application search interface showing a query input to search from the RAG database." class="image--center mx-auto" width="2390" height="1900" loading="lazy"></p>
<h3 id="heading-test-document-management">Test Document Management</h3>
<ol>
<li><p>On the Documents page, click "Preview" or "View" on a document</p>
</li>
<li><p>Try downloading a document</p>
</li>
<li><p>Test deleting a document (be careful - this is permanent)</p>
</li>
</ol>
<p>If everything works correctly, you're ready to deploy your application!</p>
<h2 id="heading-step-14-deploy-your-application">Step 14: Deploy Your Application</h2>
<h3 id="heading-deploy-to-vercel">Deploy to Vercel</h3>
<p>Vercel is the easiest way to deploy Next.js applications and is made by the creators of Next.js:</p>
<p>To get started, you’ll need to push your code to GitHub. So go ahead and create a repository and push your code.</p>
<p>Then go to <a target="_blank" href="https://vercel.com/"><strong>vercel.com</strong></a> and sign in with your GitHub account. Click "New Project" and import your GitHub repository.</p>
<p>Add your environment variables in the project settings:</p>
<ul>
<li><p><code>NEXT_PUBLIC_SUPABASE_URL</code></p>
</li>
<li><p><code>NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY</code></p>
</li>
<li><p><code>SUPABASE_SERVICE_ROLE_KEY</code></p>
</li>
<li><p><code>OPENAI_API_KEY</code></p>
</li>
</ul>
<p>Then click "Deploy", and your application will be live in minutes! Vercel automatically builds and deploys your Next.js application, and you'll get a URL like <a target="_blank" href="http://your-app.vercel.app/"><code>your-app.vercel.app</code></a>.</p>
<h3 id="heading-important-deployment-notes">Important Deployment Notes</h3>
<ul>
<li><p>Make sure all environment variables are set in your Vercel project settings</p>
</li>
<li><p>The service role key is required for file uploads to work</p>
</li>
<li><p>Supabase Storage bucket should be accessible (public or with proper RLS policies)</p>
</li>
<li><p>Your OpenAI API key should have sufficient credits</p>
</li>
</ul>
<h2 id="heading-how-rag-search-works">How RAG Search Works</h2>
<p>Your application uses the RAG (Retrieval-Augmented Generation) pattern. This combines information retrieval with AI text generation. Here's how it works step by step:</p>
<ol>
<li><p><strong>Document processing</strong>: When you upload a document, it's split into chunks. These are typically 800 characters each with 100-character overlap. Each chunk gets an embedding. This is a 1536-dimensional vector that represents its semantic meaning.</p>
</li>
<li><p><strong>Storage</strong>: Embeddings are stored in a vector database. This is PostgreSQL with the pgvector extension. They're stored alongside the original text chunks. The original files are stored in Supabase Storage.</p>
</li>
<li><p><strong>Query processing</strong>: When you search, your query is converted into an embedding. It uses the same model that processed the documents. This ensures the query and documents are in the same "vector space."</p>
</li>
<li><p><strong>Similarity search</strong>: The system finds the most similar document chunks. It uses cosine similarity on the embeddings. Cosine similarity measures the angle between vectors. Smaller angles mean more similar content, even if the exact words differ.</p>
</li>
<li><p><strong>Answer generation</strong>: The retrieved chunks are used as context for an AI model. This model is GPT-4o-mini. It generates an accurate answer. The system prompt instructs the AI to only answer based on the provided context. This ensures accuracy.</p>
</li>
</ol>
<p>This approach gives you several benefits.</p>
<p>First, you get accuracy. Answers are based on your actual documents, not just the AI's training data. Second, you get transparency. You can see which document chunks were used to generate each answer. Third, you get efficiency. Only relevant chunks are used, which reduces token usage and costs. Finally, you get up-to-date information. You can update your knowledge base by uploading new documents without retraining the AI.</p>
<h2 id="heading-troubleshooting-common-issues">Troubleshooting Common Issues</h2>
<h3 id="heading-storage-rls-error-when-uploading">"Storage RLS error" when uploading</h3>
<p>This means your <code>SUPABASE_SERVICE_ROLE_KEY</code> is not set or incorrect. Make sure the key is in your <code>.env.local</code> file for local development. Also make sure you're using the service role key, not the anon key. Finally, make sure the key is correctly set in your deployment environment, such as Vercel.</p>
<h3 id="heading-failed-to-extract-text-from-file">"Failed to extract text from file"</h3>
<p>Make sure your file is a valid PDF, DOCX, or TXT file. Check that the file isn't corrupted. For PDFs, ensure they contain extractable text. Scanned PDFs with only images won't work without <a target="_blank" href="https://en.wikipedia.org/wiki/Optical_character_recognition">OCR</a>.</p>
<h3 id="heading-no-answer-generated">"No answer generated"</h3>
<p>Make sure you've uploaded at least one document. Try a different query that's more likely to match your documents. Check that embeddings were successfully created. You can verify this in your Supabase database.</p>
<h3 id="heading-vector-similarity-search-not-working">Vector similarity search not working</h3>
<p>Ensure the <code>vector</code> extension is enabled in Supabase. You can do this by running <code>CREATE EXTENSION IF NOT EXISTS vector;</code>. Verify the <code>match_documents</code> function exists in your database. You can check this in the SQL Editor. Check that embeddings are being stored correctly. They should be JSON strings in the embedding column.</p>
<h3 id="heading-slow-search-or-upload-times">Slow search or upload times</h3>
<p>Large documents take longer to process. This is because more chunks mean more embedding API calls. Consider reducing chunk size or processing documents in batches. Also check your OpenAI API rate limits.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>Now that you have a working RAG search application, you can extend it with additional features. Here are some examples of useful features you could add:</p>
<ul>
<li><p>You can add more file types by extending the text extraction to support Markdown, HTML, or other formats.</p>
</li>
<li><p>You can improve chunking by experimenting with different chunk sizes, overlap strategies, or semantic chunking.</p>
</li>
<li><p>You can add authentication to protect your documents with user authentication using Supabase Auth.</p>
</li>
<li><p>You can enhance the UI by adding features like search history, document tags, or advanced filters.</p>
</li>
<li><p>You can optimize performance by adding caching, pagination, or streaming responses.</p>
</li>
<li><p>You can add filters to allow users to search within specific documents or date ranges.</p>
</li>
<li><p>Finally, you can improve search by adding hybrid search, which combines keyword and semantic search, or reranking.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a complete RAG search application from scratch. This application demonstrates modern web development with Next.js and TypeScript. It shows vector database operations with Supabase and pgvector. It demonstrates AI integration with OpenAI embeddings and chat completions. It includes file handling and storage with Supabase Storage. Finally, it features a production-ready user interface with Tailwind CSS.</p>
<p>The RAG pattern you've implemented is used by many production applications. These include <a target="_blank" href="https://www.freecodecamp.org/news/how-to-build-an-embeddable-ai-chatbot-widget-with-cloudflare-workers/">chatbots</a>, knowledge bases, document search systems, and AI assistants. You now have the foundation to build more advanced features on top of this.</p>
<p>The skills you've learned are highly valuable in today's AI-driven development landscape. You've learned to work with embeddings, vector databases, and the RAG pattern. You can apply these concepts to build intelligent search systems, document Q&amp;A applications, or AI-powered knowledge bases.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Supabase for Beginners ]]>
                </title>
                <description>
                    <![CDATA[ Supabase is an open-source Backend-as-a-Service (BaaS) platform that provides developers with a powerful set of tools to build applications quickly. This comprehensive video tutorial introduces you to Supabase by building a real-world React.js Sales ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/supabase-for-beginners/</link>
                <guid isPermaLink="false">694189cc7d760e7dd4cabc0a</guid>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Tue, 16 Dec 2025 16:33:16 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1765925162547/def59028-379b-4324-93b2-79cea57c3b35.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Supabase is an open-source Backend-as-a-Service (BaaS) platform that provides developers with a powerful set of tools to build applications quickly.</p>
<p>This comprehensive video tutorial introduces you to Supabase by building a real-world React.js Sales Dashboard app with authentication, real-time data operations, and secure user management.</p>
<p>The app features user authentication with role-based access along with a visualization of aggregated sales deals for each rep, updating in real-time as new deals are added. You’ll also build scalable user profiles which are automatically created upon signup using database triggers, and secure all data with permission-based filtering. By the end, you’ll have a production-ready business application with proper user management, data visualization, and security.</p>
<p>Here are the topics covered in this course:</p>
<p><strong>Database &amp; Schema Design</strong></p>
<ul>
<li><p>Create Supabase projects with multiple related tables</p>
</li>
<li><p>Design user profiles with account type categorization</p>
</li>
<li><p>Set up database triggers for automated data population</p>
</li>
</ul>
<p><strong>Authentication &amp; Session Management</strong></p>
<ul>
<li><p>Configure Supabase Auth for user signup and sign in/out</p>
</li>
<li><p>Handle user sessions and JWT authentication</p>
</li>
<li><p>Connect authenticated users to their profiles</p>
</li>
</ul>
<p><strong>Data Operations &amp; Real-time Features</strong></p>
<ul>
<li><p>Query databases to fetch and display data</p>
</li>
<li><p>Aggregate sales data for visualization</p>
</li>
<li><p>Create bar chart visualizations of deals by rep</p>
</li>
<li><p>Insert new records via forms</p>
</li>
<li><p>Set up Realtime subscriptions for live chart updates</p>
</li>
</ul>
<p>Watch the full course on <a target="_blank" href="https://youtu.be/Q7P20fHJlm4">the freeCodeCamp.org YouTube channel</a> (5-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Q7P20fHJlm4" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Secure SSR Authentication with Supabase, Astro, and Cloudflare Turnstile ]]>
                </title>
                <description>
                    <![CDATA[ In this guide, you'll build a full server-side rendered (SSR) authentication system using Astro, Supabase, and Cloudflare Turnstile to protect against bots. By the end, you'll have a fully functional authentication system with Astro actions, magic li... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-secure-ssr-authentication-with-supabase-astro-and-cloudflare-turnstile/</link>
                <guid isPermaLink="false">685594145aea0dba325c37e1</guid>
                
                    <category>
                        <![CDATA[ supabase ss ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Astro ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase auth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ magic links ]]>
                    </category>
                
                    <category>
                        <![CDATA[ cloudflare ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloudflare Turnstile ]]>
                    </category>
                
                    <category>
                        <![CDATA[ SSR ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Fatuma Abdullahi ]]>
                </dc:creator>
                <pubDate>Fri, 20 Jun 2025 17:02:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750438909287/d36c0c01-e779-4eea-aa41-b797fcbb05f6.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this guide, you'll build a full server-side rendered (SSR) authentication system using Astro, Supabase, and Cloudflare Turnstile to protect against bots.</p>
<p>By the end, you'll have a fully functional authentication system with Astro actions, magic link authentication using Supabase, bot protection via Cloudflare Turnstile, protected routes and middleware, and secure session management.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-the-technologies">Understanding the Technologies</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-astro">What is Astro?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-astro-actions">What are Astro Actions?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-supabase">What is Supabase?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-cloudflare-turnstile">What is Cloudflare Turnstile?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-ssr-authentication">Understanding SSR Authentication</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-ssr-vs-spa-authentication">SSR vs. SPA Authentication</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-why-protect-auth-forms">Why Protect Auth Forms?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-part-1-how-to-set-up-the-backend">Part 1: How to Set Up the Backend</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-set-up-supabase-backend">Set Up Supabase Backend</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-cloudflare-turnstile">Set Up Cloudflare Turnstile</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-2-how-to-set-up-the-frontend">Part 2: How to Set Up the Frontend</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-astro-project">Create the Astro Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-astro-for-ssr">Configure Astro for SSR</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-install-supabase-dependencies">Install Supabase Dependencies</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-environment-variables">Configure Environment Variables</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-3-how-to-set-up-supabase-ssr">Part 3: How to Set Up Supabase SSR</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-supabase-client">Create the Supabase Client</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-middleware-for-route-protection">Create Middleware for Route Protection</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-4-how-to-build-the-user-interface">Part 4: How to Build the User Interface</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-update-the-layout">Update the Layout</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-sign-in-page">Create the Sign-In Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-protected-page">Create the Protected Page</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-5-how-to-set-up-astro-actions">Part 5: How to Set Up Astro Actions</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-create-the-authentication-actions">Create the Authentication Actions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-code-exchange-api-route">Create the Code Exchange API Route</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-part-6-how-to-test-your-application">Part 6: How to Test Your Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-notes-and-additional-resources">Notes and Additional Resources</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-useful-documentation">Useful Documentation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-complete-code-repository">Complete Code Repository</a></p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This tutorial assumes you are familiar with:</p>
<ul>
<li><p>Web development frameworks</p>
</li>
<li><p><a target="_blank" href="https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/">Basic authentication flows</a></p>
</li>
<li><p>Basic Backend-as-a-Service (BaaS) concepts</p>
</li>
</ul>
<h2 id="heading-understanding-the-technologies">Understanding the Technologies</h2>
<h3 id="heading-what-is-astro">What is Astro?</h3>
<p><a target="_blank" href="https://docs.astro.build/en/getting-started/">Astro</a> is a UI-agnostic web framework that renders <a target="_blank" href="https://docs.astro.build/en/concepts/why-astro/#server-first">server-first</a> by default. It <a target="_blank" href="https://docs.astro.build/en/guides/integrations-guide/#official-integrations">can be used with any UI framework</a>, including <a target="_blank" href="https://docs.astro.build/en/guides/client-side-scripts/">Astro client components</a>.</p>
<h3 id="heading-what-are-astro-actions">What are Astro Actions?</h3>
<p><a target="_blank" href="https://docs.astro.build/en/guides/actions/">Astro actions</a> allow you to write server-side functions that can be called without explicitly setting up API routes. They provide many useful utilities that simplify the process of running server logic and can be called from both client and server environments.</p>
<h3 id="heading-what-is-supabase">What is Supabase?</h3>
<p><a target="_blank" href="https://supabase.com/docs">Supabase</a> is an open-source Backend-as-a-Service that builds upon <a target="_blank" href="https://www.postgresql.org/docs/">Postgres</a>. It provides key features such as authentication, real-time capabilities, edge functions, storage, and more. Supabase offers both a hosted version for easy scaling and a self-hostable version for full control.</p>
<h3 id="heading-what-is-cloudflare-turnstile">What is Cloudflare Turnstile?</h3>
<p>Turnstile is <a target="_blank" href="https://www.cloudflare.com/en-gb/application-services/products/turnstile/">Cloudflare's replacement for CAPTCHAs</a>, which are visual puzzles used to differentiate between genuine users and bots. Unlike traditional CAPTCHAs, which are visually clunky, annoying, and <a target="_blank" href="https://blog.cloudflare.com/turnstile-ga/">sometimes difficult to solve</a>, Turnstile detects malicious activity without requiring users to solve puzzles, while providing a better user experience.</p>
<h2 id="heading-understanding-ssr-authentication">Understanding SSR Authentication</h2>
<p>Server-side rendered (SSR) auth refers to handling authentication on the server using a <a target="_blank" href="https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/#how-does-authentication-work">cookie-based authentication method</a>.</p>
<p>The flow works as follows:</p>
<ol>
<li><p>The server creates a session and stores a session ID in a cookie sent to the client</p>
</li>
<li><p>The browser receives the cookie and automatically includes it in future requests</p>
</li>
<li><p>The server uses the cookie to determine if the user is authenticated</p>
</li>
</ol>
<p>Since browsers cannot modify HTTP-only cookies and servers cannot access local storage, SSR authentication requires careful management to prevent security risks such as session hijacking and stale sessions.</p>
<h3 id="heading-ssr-vs-spa-authentication">SSR vs. SPA Authentication</h3>
<p>Single-Page Applications (SPAs), like traditional React apps, handle authentication on the client side because they don't have direct access to a server. SPAs typically use JWTs stored in local storage, cookies, or session storage, sending these tokens in HTTP headers when communicating with servers.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/HdE3dk8VkRU" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-why-protect-auth-forms">Why Protect Auth Forms?</h2>
<p>Authentication protects sensitive resources from unauthorized access, making auth forms primary targets for bots and malicious actors. Taking extra precautions is important for maintaining security.</p>
<h2 id="heading-part-1-how-to-set-up-the-backend">Part 1: How to Set Up the Backend</h2>
<h3 id="heading-set-up-supabase-backend">Set Up Supabase Backend</h3>
<p>First, you'll need <a target="_blank" href="https://supabase.com/dashboard/">a Supabase account</a>. Create a project, then:</p>
<ol>
<li><p>Go to the Authentication tab in the sidebar</p>
</li>
<li><p>Click the Sign In / Up tab under Configuration</p>
</li>
<li><p>Enable user sign-ups</p>
</li>
<li><p>Scroll down to Auth Providers and enable email (disable email confirmation for this tutorial)</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742054137964/a379192b-4eaf-491f-bcf4-a0e1e0deef94.png" alt="Supabase authentication configuration interface showing user signup options and email provider enabled" width="2480" height="1448" loading="lazy"></p>
<h3 id="heading-set-up-cloudflare-turnstile">Set Up Cloudflare Turnstile</h3>
<ol>
<li><p><a target="_blank" href="https://dash.cloudflare.com/login">Log in or register for a Cloudflare account</a></p>
</li>
<li><p>Click the Turnstile tab in the sidebar</p>
</li>
<li><p>Click the "Add widget" button</p>
</li>
<li><p>Name your widget and add "localhost" as the hostname</p>
</li>
<li><p>Leave all other settings as default, and create the widget</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750260766060/95ec02e5-8ee7-4438-a66c-76866ec068c1.png" alt="Cloudflare Turnstile widget creation interface" width="2200" height="1796" loading="lazy"></p>
<p>After creating the widget, copy the secret key and add it to your Supabase dashboard:</p>
<ol>
<li><p>Go back to Supabase Authentication settings</p>
</li>
<li><p>Navigate to the Auth Protection tab under Configuration</p>
</li>
<li><p>Turn on Captcha protection</p>
</li>
<li><p>Choose Cloudflare as the provider</p>
</li>
<li><p>Paste your secret key</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750260776990/56ef5fc1-3321-45f0-ab9a-878679a08e88.png" alt="Supabase Attack Protection settings with Turnstile configuration" width="2302" height="986" loading="lazy"></p>
<h2 id="heading-part-2-how-to-set-up-the-frontend">Part 2: How to Set Up the Frontend</h2>
<h3 id="heading-create-the-astro-project">Create the Astro Project</h3>
<p>Next, you will need to create an Astro project. Open your preferred IDE or Text editor’s integrated terminal and run the following command to scaffold an Astro project in a folder named “ssr-auth.” Feel free to use any name you like.</p>
<pre><code class="lang-bash">npm create astro@latest ssr-auth
</code></pre>
<p>Follow the provided prompts and choose a basic template to start with. When it’s done, change into the folder, then run <code>npm install</code> to install dependencies, followed by <code>npm run dev</code> to start the server, and your site will be available at <a target="_blank" href="http://localhost:4321"><code>localhost:4321</code></a>.</p>
<h3 id="heading-configure-astro-for-ssr">Configure Astro for SSR</h3>
<p>Set Astro to run in SSR mode by adding <code>output: "server",</code> to the <code>defineConfig</code> function found in the <code>astro.config.mjs</code> file at the root of the folder.</p>
<p>Next, <a target="_blank" href="https://docs.astro.build/en/guides/integrations-guide/node/">add an adapter</a> to create a server runtime. For this, use the Node.js adapter by running this command in a terminal: <code>npx astro add node</code>. This will add it and automatically make all relevant changes.</p>
<p>Finally, add Tailwind for styling. Run this command in a terminal window: <code>npx astro add tailwind</code>. Follow the prompts, and it will make any changes necessary.</p>
<p>At this stage, your <code>astro.config.mjs</code> should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// @ts-check</span>
<span class="hljs-keyword">import</span> { defineConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro/config"</span>;
<span class="hljs-keyword">import</span> node <span class="hljs-keyword">from</span> <span class="hljs-string">"@astrojs/node"</span>;
<span class="hljs-keyword">import</span> tailwindcss <span class="hljs-keyword">from</span> <span class="hljs-string">"@tailwindcss/vite"</span>;

<span class="hljs-comment">// https://astro.build/config</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> defineConfig({
  output: <span class="hljs-string">"server"</span>,
  adapter: node({
    mode: <span class="hljs-string">"standalone"</span>,
  }),
  vite: {
    plugins: [tailwindcss()],
  },
});
</code></pre>
<h3 id="heading-install-supabase-dependencies">Install Supabase Dependencies</h3>
<p>You can do this by running the following command:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js @supabase/ssr
</code></pre>
<h3 id="heading-configure-environment-variables">Configure Environment Variables</h3>
<p>Create a <code>.env</code> file in the project root and add the following. Remember to replace with your actual credentials:</p>
<pre><code class="lang-bash">SUPABASE_URL=&lt;YOUR_URL&gt;
SUPABASE_ANON_KEY=&lt;YOUR_ANON_KEY&gt;
TURNSTILE_SITE_KEY=&lt;YOUR_TURNSTILE_SITE_KEY&gt;
</code></pre>
<p>You can get the Supabase values from the dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1742054292788/8aeec326-259c-49bd-a6f8-b885e9a9e6ea.png" alt="Supabase project connection interface showing environment variables" width="2132" height="802" loading="lazy"></p>
<p><strong>💡Note:</strong> In Astro, environment variables accessed on the client side must be prefixed with 'PUBLIC'. But since we're using Astro actions that run on the server, the prefix is not required.</p>
<h2 id="heading-part-3-how-to-set-up-supabase-ssr">Part 3: How to Set Up Supabase SSR</h2>
<h3 id="heading-create-the-supabase-client">Create the Supabase Client</h3>
<p>Create <code>src/lib/supabase.ts</code>:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { createServerClient, parseCookieHeader } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AstroCookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createClient</span>(<span class="hljs-params">{
    request,
    cookies,
}: {
    request: Request;
    cookies: AstroCookies;
}</span>) </span>{
    <span class="hljs-keyword">const</span> cookieHeader = request.headers.get(<span class="hljs-string">"Cookie"</span>) || <span class="hljs-string">""</span>;

    <span class="hljs-keyword">return</span> createServerClient(
        <span class="hljs-keyword">import</span>.meta.env.SUPABASE_URL,
        <span class="hljs-keyword">import</span>.meta.env.SUPABASE_ANON_KEY,
        {
            cookies: {
                getAll() {
                    <span class="hljs-keyword">const</span> cookies = parseCookieHeader(cookieHeader);
                    <span class="hljs-keyword">return</span> cookies.map(<span class="hljs-function">(<span class="hljs-params">{ name, value }</span>) =&gt;</span> ({
                        name,
                        value: value ?? <span class="hljs-string">""</span>,
                    }));
                },
                setAll(cookiesToSet) {
                    cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
                        cookies.set(name, value, options)
                    );
                },
            },
        }
    );
}
</code></pre>
<p>This sets up Supabase to handle <a target="_blank" href="https://supabase.com/docs/guides/auth/server-side/creating-a-client?queryGroups=framework&amp;framework=astro&amp;queryGroups=environment&amp;environment=astro-browser">cookies in a server-rendered application</a> and exports a function that takes the request and cookies object as input. The function is set up like this because Astro has three ways to access request and cookie information:</p>
<ul>
<li><p>Through Astro’s global object, which is only available on Astro pages.</p>
</li>
<li><p>Through <code>AstroAPIContext</code> object, which is only available in Astro actions.</p>
</li>
<li><p>Through <code>APIContext</code> which is a subset of the global object and is available through API routes and middleware.</p>
</li>
</ul>
<p>So the <code>createClient</code> function accepts the <code>request</code> and <code>cookies</code> objects separately to make it flexible and applicable in the various contexts in which it may be used.</p>
<h3 id="heading-create-middleware-for-route-protection">Create Middleware for Route Protection</h3>
<p>Next, create a <code>middleware.ts</code> file in the <code>src</code> folder and paste this into it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineMiddleware } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:middleware"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"./lib/supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> onRequest = defineMiddleware(<span class="hljs-keyword">async</span> (context, next) =&gt; {
    <span class="hljs-keyword">const</span> { pathname } = context.url;

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Middleware executing for path:"</span>, pathname);

    <span class="hljs-keyword">const</span> supabase = createClient({
        request: context.request,
        cookies: context.cookies,
    });

    <span class="hljs-keyword">if</span> (pathname === <span class="hljs-string">"/protected"</span>) {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Checking auth for protected route"</span>);

        <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();

        <span class="hljs-comment">// If no user, redirect to index</span>
        <span class="hljs-keyword">if</span> (!data.user) {
            <span class="hljs-keyword">return</span> context.redirect(<span class="hljs-string">"/"</span>);
        }
    }

    <span class="hljs-keyword">return</span> next();
});
</code></pre>
<p>This middleware checks for an active user when accessing the protected route and redirects unauthenticated users to the index page.</p>
<h2 id="heading-part-4-how-to-build-the-user-interface">Part 4: How to Build the User Interface</h2>
<h3 id="heading-update-the-layout">Update the Layout</h3>
<p>First, update <code>src/layouts/Layout.astro</code> to include the Turnstile script. Add this just above the closing <code>&lt;/head&gt;</code> tag:</p>
<pre><code class="lang-typescript">&lt;script
    src=<span class="hljs-string">"https://challenges.cloudflare.com/turnstile/v0/api.js"</span>
    <span class="hljs-keyword">async</span>
    defer&gt;
&lt;/script&gt;
</code></pre>
<h3 id="heading-create-the-sign-in-page">Create the Sign-In Page</h3>
<p>Replace the contents of <code>src/pages/index.astro</code>:</p>
<pre><code class="lang-typescript">---
<span class="hljs-keyword">import</span> Layout <span class="hljs-keyword">from</span> <span class="hljs-string">"../layouts/Layout.astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/global.css"</span>;

<span class="hljs-keyword">const</span> supabase = createClient({
    request: Astro.request,
    cookies: Astro.cookies,
});

<span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();

<span class="hljs-keyword">if</span> (data.user) {
    <span class="hljs-keyword">return</span> Astro.redirect(<span class="hljs-string">"/protected"</span>);
}

<span class="hljs-keyword">const</span> apiKey = <span class="hljs-keyword">import</span>.meta.env.TURNSTILE_SITE_KEY;
---

&lt;Layout&gt;
    &lt;section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>&gt;
        &lt;h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>&gt;Sign In to Your Account&lt;/h1&gt;
        &lt;form id=<span class="hljs-string">"signin-form"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col gap-2 w-1/2"</span>&gt;
            &lt;label <span class="hljs-keyword">for</span>=<span class="hljs-string">"email"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">""</span>&gt;Enter your email&lt;/label&gt;
            &lt;input
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"email"</span>
                name=<span class="hljs-string">"email"</span>
                id=<span class="hljs-string">"email"</span>
                placeholder=<span class="hljs-string">"youremail@example.com"</span>
                <span class="hljs-keyword">class</span>=<span class="hljs-string">"border border-gray-500 rounded-md p-2"</span>
                required
            /&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"cf-turnstile"</span> data-sitekey={apiKey}&gt;&lt;/div&gt;
            &lt;button
                <span class="hljs-keyword">type</span>=<span class="hljs-string">"submit"</span>
                id=<span class="hljs-string">"sign-in"</span>
                <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-gray-600 hover:bg-gray-700 p-2 rounded-md text-white font-bold w-full cursor-pointer disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"</span>
                &gt;Sign In&lt;/button
            &gt;
        &lt;/form&gt;
    &lt;/section&gt;
&lt;/Layout&gt;
</code></pre>
<p>Here, the frontmatter creates a Supabase server client and then uses it to check if we have an active user. It redirects based on this information. This works because the front matter runs on the server side, and the project is set to server output.</p>
<p>The template displays a simple form with an email input. To complete it, add this below the closing <code>&lt;/Layout&gt;</code> tag:</p>
<pre><code class="lang-typescript">
&lt;script&gt;
    <span class="hljs-keyword">import</span> { actions } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;

    <span class="hljs-keyword">declare</span> <span class="hljs-built_in">global</span> {
        <span class="hljs-keyword">interface</span> Window {
            turnstile?: {
                reset: <span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">void</span>;
            };
        }
    }

    <span class="hljs-keyword">const</span> signInForm = <span class="hljs-built_in">document</span>.querySelector(<span class="hljs-string">"#signin-form"</span>) <span class="hljs-keyword">as</span> HTMLFormElement;
    <span class="hljs-keyword">const</span> formSubmitBtn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"sign-in"</span>) <span class="hljs-keyword">as</span> HTMLButtonElement;

    signInForm?.addEventListener(<span class="hljs-string">"submit"</span>, <span class="hljs-keyword">async</span> (e) =&gt; {
        e.preventDefault();
        formSubmitBtn.disabled = <span class="hljs-literal">true</span>;
        formSubmitBtn.textContent = <span class="hljs-string">"Signing in..."</span>;

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> turnstileToken = (
                <span class="hljs-built_in">document</span>.querySelector(
                    <span class="hljs-string">"[name='cf-turnstile-response']"</span>
                ) <span class="hljs-keyword">as</span> HTMLInputElement
            )?.value;

            <span class="hljs-keyword">if</span> (!turnstileToken) {
                <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"verification_missing"</span>);
            }

            <span class="hljs-keyword">const</span> formData = <span class="hljs-keyword">new</span> FormData(signInForm);
            formData.append(<span class="hljs-string">"captchaToken"</span>, turnstileToken);

            <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> actions.signIn(formData);

            <span class="hljs-keyword">if</span> (!results.data?.success) {
                <span class="hljs-keyword">if</span> (results.data?.message?.includes(<span class="hljs-string">"captcha protection"</span>)) {
                    alert(<span class="hljs-string">"Verification failed. Please try again."</span>);
                    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.turnstile) {
                        <span class="hljs-built_in">window</span>.turnstile.reset();
                    }
                    formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
                    formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
                    <span class="hljs-keyword">return</span>;
                } <span class="hljs-keyword">else</span> {
                    alert(<span class="hljs-string">"Oops! Could not sign in. Please try again"</span>);
                    formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
                    formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
                    <span class="hljs-keyword">return</span>;
                }
            }

            formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
            alert(<span class="hljs-string">"Please check your email to sign in"</span>);
        } <span class="hljs-keyword">catch</span> (error) {
            <span class="hljs-keyword">if</span> (<span class="hljs-built_in">window</span>.turnstile) {
                <span class="hljs-built_in">window</span>.turnstile.reset();
            }
            formSubmitBtn.disabled = <span class="hljs-literal">false</span>;
            formSubmitBtn.textContent = <span class="hljs-string">"Sign In"</span>;
            <span class="hljs-built_in">console</span>.log(error);
            alert(<span class="hljs-string">"Something went wrong. Please try again"</span>);
        }
    });
&lt;/script&gt;
</code></pre>
<p>This adds some vanilla JavaScript that calls the <code>SignIn</code> Upon form submission. This action provides user feedback through alerts and manages the button’s text and disabled state. This effectively adds client-side interactivity to the page.</p>
<h3 id="heading-create-the-protected-page">Create the Protected Page</h3>
<p>Create <code>src/pages/protected.astro</code>:</p>
<pre><code class="lang-typescript">---
<span class="hljs-keyword">import</span> Layout <span class="hljs-keyword">from</span> <span class="hljs-string">"../layouts/Layout.astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"../styles/global.css"</span>;

<span class="hljs-keyword">const</span> supabase = createClient({
    request: Astro.request,
    cookies: Astro.cookies,
});

<span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> supabase.auth.getUser();
---

&lt;Layout&gt;
    &lt;section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>&gt;
        &lt;h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>&gt;You are logged <span class="hljs-keyword">in</span>!&lt;/h1&gt;
        &lt;p <span class="hljs-keyword">class</span>=<span class="hljs-string">"mb-6"</span>&gt;Your user Id: {data.user?.id}&lt;/p&gt;
        &lt;button
            id=<span class="hljs-string">"sign-out"</span>
            <span class="hljs-keyword">class</span>=<span class="hljs-string">"bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-md text-white font-bold cursor-pointer disabled:bg-gray-500 disabled:hover:bg-gray-500 disabled:cursor-not-allowed"</span>
            &gt;Sign Out&lt;/button
        &gt;
    &lt;/section&gt;
&lt;/Layout&gt;

&lt;script&gt;
    <span class="hljs-keyword">import</span> { actions } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;
    <span class="hljs-keyword">const</span> signOutBtn = <span class="hljs-built_in">document</span>.getElementById(<span class="hljs-string">"sign-out"</span>) <span class="hljs-keyword">as</span> HTMLButtonElement;

    signOutBtn?.addEventListener(<span class="hljs-string">"click"</span>, <span class="hljs-keyword">async</span> (e) =&gt; {
        e.preventDefault();
        signOutBtn!.disabled = <span class="hljs-literal">true</span>;
        signOutBtn!.textContent = <span class="hljs-string">"Signing out..."</span>;

        <span class="hljs-keyword">try</span> {
            <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> actions.signOut();

            <span class="hljs-keyword">if</span> (!results.data?.success) {
                signOutBtn!.disabled = <span class="hljs-literal">false</span>;
                signOutBtn!.textContent = <span class="hljs-string">"Sign Out"</span>;
                <span class="hljs-keyword">return</span> alert(<span class="hljs-string">"Oops! Could not sign Out. Please try again"</span>);
            }
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">window</span>.location.reload();
        } <span class="hljs-keyword">catch</span> (error) {
            signOutBtn.disabled = <span class="hljs-literal">false</span>;
            signOutBtn.textContent = <span class="hljs-string">"Sign Out"</span>;
            <span class="hljs-built_in">console</span>.log(error);
            <span class="hljs-keyword">return</span> alert(<span class="hljs-string">"Something went wrong. Please try again"</span>);
        }
    });
&lt;/script&gt;
</code></pre>
<p>This page retrieves the user data server-side in the front matter and displays it in the template, along with a sign-out button.</p>
<p>The JavaScript in the <code>script</code> tags handle calling the sign-out action, user feedback, and button state, as in the <code>index.astro</code> page.</p>
<h2 id="heading-part-5-how-to-set-up-astro-actions">Part 5: How to Set Up Astro Actions</h2>
<h3 id="heading-create-the-authentication-actions">Create the Authentication Actions</h3>
<p>Finally, add an <code>actions</code> folder in the <code>src</code> folder and create an <code>index.ts</code> file to hold our logic. Paste the following into it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { defineAction, <span class="hljs-keyword">type</span> ActionAPIContext } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:actions"</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro:schema"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-keyword">const</span> emailSignUp = <span class="hljs-keyword">async</span> (
    {
        email,
        captchaToken,
    }: {
        email: <span class="hljs-built_in">string</span>;
        captchaToken: <span class="hljs-built_in">string</span>;
    },
    context: ActionAPIContext
) =&gt; {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Sign up action"</span>);
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> supabase = createClient({
            request: context.request,
            cookies: context.cookies,
        });

        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithOtp({
            email,
            options: {
                captchaToken,
                emailRedirectTo: <span class="hljs-string">"http://localhost:4321/api/exchange"</span>,
            },
        });

        <span class="hljs-keyword">if</span> (error) {
            <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Sign up error"</span>, error);
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">false</span>,
                message: error.message,
            };
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Sign up success"</span>, data);
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">true</span>,
                message: <span class="hljs-string">"Successfully logged in"</span>,
            };
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"SignUp action other error"</span>, err);
        <span class="hljs-keyword">return</span> {
            success: <span class="hljs-literal">false</span>,
            message: <span class="hljs-string">"Unexpected error"</span>,
        };
    }
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> server = {
    signIn: defineAction({
        accept: <span class="hljs-string">"form"</span>,
        input: z.object({
            email: z.string().email(),
            captchaToken: z.string(),
        }),
        handler: <span class="hljs-keyword">async</span> (input, context) =&gt; {
            <span class="hljs-keyword">return</span> emailSignUp(input, context);
        },
    }),
    signOut: defineAction({
        handler: <span class="hljs-keyword">async</span> (_, context) =&gt; {
            <span class="hljs-keyword">const</span> supabase = createClient({
                request: context.request,
                cookies: context.cookies,
            });
            <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
            <span class="hljs-keyword">if</span> (error) {
                <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Sign out error"</span>, error);
                <span class="hljs-keyword">return</span> {
                    success: <span class="hljs-literal">false</span>,
                    message: error.message,
                };
            }
            <span class="hljs-keyword">return</span> {
                success: <span class="hljs-literal">true</span>,
                message: <span class="hljs-string">"Successfully signed out"</span>,
            };
        },
    }),
};
</code></pre>
<p>This action handles both sign-in and sign-out methods. A Supabase server instance is created during the sign-in method, and the magic link method is used for sign-in. It passes a redirect URL, which we have yet to create, and handles any errors that may occur.</p>
<p>It also passes the token verification, allowing Supabase to perform verification on our behalf, eliminating the need to call <a target="_blank" href="https://developers.cloudflare.com/turnstile/get-started/server-side-validation/">Cloudflare’s verify APIs</a> directly.</p>
<p>The sign-out method calls Supabase’s sign-out method and handles any potential errors.</p>
<p>The redirect URL refers to an API route that exchanges the code from the email Supabase sends for a session that Supabase handles.</p>
<h3 id="heading-create-the-code-exchange-api-route">Create the Code Exchange API Route</h3>
<p>Create <code>src/pages/api/exchange.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { APIRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"astro"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> GET: APIRoute = <span class="hljs-keyword">async</span> ({ request, cookies, redirect }) =&gt; {
    <span class="hljs-keyword">const</span> url = <span class="hljs-keyword">new</span> URL(request.url);
    <span class="hljs-keyword">const</span> code = url.searchParams.get(<span class="hljs-string">"code"</span>);

    <span class="hljs-keyword">if</span> (!code) {
        <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/"</span>);
    }

    <span class="hljs-keyword">const</span> supabase = createClient({ request, cookies });
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.exchangeCodeForSession(code);

    <span class="hljs-keyword">if</span> (error) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error exchanging code for session:"</span>, error);
        <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/404"</span>);
    }

    <span class="hljs-keyword">return</span> redirect(<span class="hljs-string">"/protected"</span>);
};
</code></pre>
<p>This grabs the code from the URL in the magic link sent, creates a server client, and calls the <code>exchangeCodeForSession</code> method with the code. It handles any error by redirecting to Astro’s built-in not-found page.</p>
<p>Otherwise, it will redirect to the protected page as Supabase handles the session implementation details.</p>
<h2 id="heading-part-6-how-to-test-your-application">Part 6: How to Test Your Application</h2>
<p>Start your development server: <code>npm run dev</code></p>
<p>Visit the provided localhost URL. You should see the sign-in page with the Turnstile widget:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750267075336/66ad5f39-67c6-458a-96ea-4dfe1123b015.png" alt="Sign-in page with Turnstile verification and email input field" width="2356" height="956" loading="lazy"></p>
<p>If you try to access the <code>/protected</code> page, it will redirect you back to this view until you sign in. Now, sign in, and you should get an email with a link that will redirect you to the <code>/protected</code> page. This is what you should see:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1750335131827/f85cde2f-f9bb-46b0-a09e-6ae6456cd49f.png" alt="Text reads: &quot;You are logged in!&quot; with a field labeled &quot;Your user Id&quot; and a &quot;Sign Out&quot; button below." width="1200" height="502" loading="lazy"></p>
<p>And with that, you've successfully built a comprehensive auth system that leverages Astro actions, Supabase auth, and Cloudflare Turnstile's bot protection. This setup provides a secure, user-friendly authentication experience while protecting your application from malicious actors.</p>
<h2 id="heading-notes-and-additional-resources">Notes and Additional Resources</h2>
<h3 id="heading-useful-documentation">Useful Documentation</h3>
<ul>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/auth/server-side/advanced-guide">Supabase's advanced guide to SSR</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/supabase/ssr">Supabase SSR package</a></p>
</li>
<li><p><a target="_blank" href="https://docs.astro.build/en/reference/api-reference/#cookies">Astro Cookies documentation</a></p>
</li>
<li><p><a target="_blank" href="https://supabase.com/docs/guides/auth/sessions/pkce-flow">Supabase PKCE flow documentation</a></p>
</li>
<li><p><a target="_blank" href="https://docs.astro.build/en/guides/actions/">Astro Actions documentation</a></p>
</li>
<li><p><a target="_blank" href="https://developers.cloudflare.com/turnstile/get-started/">Get started with Turnstile</a></p>
</li>
</ul>
<h3 id="heading-complete-code-repository">Complete Code Repository</h3>
<p>The complete code for this project is available on GitHub:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/FatumaA/supa-ssr">Base authentication setup</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/FatumaA/supa-ssr/tree/add-cloudflare">With Cloudflare Turnstile</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Realtime Chat Application with Angular 20 and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ Chat applications let you talk in real-time with your friends, family, or coworkers, and help you quickly, effectively, and efficiently transfer of information. When you’re building modern web applications, chat applications are now pretty much a req... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-realtime-chat-app-with-angular-20-and-supabase/</link>
                <guid isPermaLink="false">6850913c10d2d4fd5525e0d6</guid>
                
                    <category>
                        <![CDATA[ Angular ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Angular ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ deji adesoga ]]>
                </dc:creator>
                <pubDate>Mon, 16 Jun 2025 21:48:44 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1750094888966/7ac31fee-bd4d-4353-b8cb-911ac60b4516.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Chat applications let you talk in real-time with your friends, family, or coworkers, and help you quickly, effectively, and efficiently transfer of information. When you’re building modern web applications, chat applications are now pretty much a requirement to enable collaboration and enhanced the user experience.</p>
<p>In this tutorial, we will break down how to build a chat application using modern technologies like Angular and Supabase. Building this chat application will help you learn features such as Google OAuth 2.0 for authentication, Angular router for navigation, the <code>CanActivate</code> route guard for route protection, and how to call Supabase functions to create, fetch and delete chats.</p>
<p>On the backend, you will learn how to create database tables in Supabase. You’ll also learn about Supabase functions and Supabase triggers.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-table-of-contents">Table of Contents</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-installations-and-account-configuration">Installations and Account Configuration:</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-the-user-interface-of-the-angular-application">How to Create the User Interface of the Angular Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-a-new-supabase-project">How to Set Up a New Supabase Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-google-oauth-20-for-authentication-and-authorization">How to Set Up Google OAuth 2.0 for Authentication and Authorization</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-the-router-of-the-angular-application">How to Configure the Router of the Angular Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-set-up-the-authentication-service">How to Set Up the Authentication Service</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-create-the-service-functions-for-login-and-sign-out-functionality">How to Create the Service Functions for Login and Sign Out Functionality</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-the-authentication-service-function-in-the-template">How to Integrate the Authentication Service Function in the Template</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-route-protection-in-angular">How to Create Route Protection in Angular</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-and-setup-the-users-table-in-supabase-using-the-sql-editor">How to Create and Setup the Users Table in Supabase using the SQL Editor</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-row-level-security-policies-in-supabase-with-the-sql-editor">How to Configure Row Level Security Policies in Supabase with the SQL Editor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-supabase-functions-in-supabase-with-the-sql-editor">How to Configure Supabase Functions in Supabase with the SQL Editor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-configure-supabase-trigger-in-supabase-with-the-sql-editor">How to Configure Supabase Trigger in Supabase with the SQL Editor</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-and-setup-the-chat-table-in-supabase-using-the-user-interface">How to Create and Setup the Chat Table in Supabase using the User Interface</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-create-and-setup-the-chat-table-policies-in-supabase">How to Create and Setup the Chat Table Policies in Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-integrate-functionality-to-create-a-new-chat-message-in-the-angular-application">How to Integrate Functionality to Create a New Chat Message in the Angular Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-fetch-data-in-the-angular-application-from-supabase">How to Fetch Data in the Angular Application from Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-delete-data-in-the-angular-application">How to Delete Data in the Angular Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-implement-logout-functionality-in-the-angular-application">How to Implement Logout Functionality in the Angular Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<ul>
<li><p>HTML</p>
</li>
<li><p>JavaScript</p>
</li>
<li><p>TypeScript</p>
</li>
</ul>
<h2 id="heading-installations-and-account-configuration"><strong>Installations and Account Configuration:</strong></h2>
<p>Before we begin, make sure you have the following installed and ready:</p>
<ul>
<li><p><strong>Node.js and npm:</strong> Angular requires Node. You can check to see if you have it (and what version you have) by running <code>node -v</code> in your terminal.</p>
</li>
<li><p><strong>Angular CLI:</strong> This is the command-line tool to scaffold and manage Angular projects. If you don’t have it, install it with <code>npm install -g @angular/cli</code>. Verify with <code>ng version</code>.</p>
</li>
<li><p><strong>A Supabase account:</strong> Supabase offers a free tier. Sign up on the <a target="_blank" href="http://supabase.com/">Supabase</a> website if you haven’t already.</p>
</li>
</ul>
<p>You can also watch the video version of this article below, or on my <a target="_blank" href="https://youtu.be/8SRhekaJ5iI?si=Vddj2ayZ0rF1R3W2">YouTube channel</a>:</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/8SRhekaJ5iI" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<p> </p>
<h2 id="heading-how-to-create-the-user-interface-of-the-angular-application">How to Create the User Interface of the Angular Application</h2>
<p>To create the user interface of the application, we’ll use <a target="_blank" href="https://getbootstrap.com/docs/5.0/getting-started/introduction/">Bootstrap 5</a>. In the <code>index.html</code> file of the Angular application, you are going to paste the Bootstrap 5 CDN link as seen below:</p>
<pre><code class="lang-xml"><span class="hljs-meta">&lt;!doctype <span class="hljs-meta-keyword">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"utf-8"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>NgChat<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">base</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"icon"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"image/x-icon"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"favicon.ico"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">link</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css"</span> <span class="hljs-attr">rel</span>=<span class="hljs-string">"stylesheet"</span>
    <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"</span> <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">app-root</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">app-root</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js"</span>
    <span class="hljs-attr">integrity</span>=<span class="hljs-string">"sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"</span>
    <span class="hljs-attr">crossorigin</span>=<span class="hljs-string">"anonymous"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>

<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span>
</code></pre>
<p>Above, you have two CDN links from Bootstrap 5. The first is the <code>&lt;link&gt;</code> tag within the head section, while the second is the <code>&lt;script&gt;</code> tag which is right below the <code>&lt;app-root&gt;&lt;/app-root&gt;</code> tag.</p>
<p>Now that you have the Bootstrap 5 CDN link setup within the project, the next step is to create two new components called <strong>chat</strong> and <strong>login</strong>, respectively, within a pages folder. You can do that using the command below:</p>
<pre><code class="lang-powershell">ng g c pages/chat<span class="hljs-literal">-component</span> &amp;&amp; ng g c pages/login<span class="hljs-literal">-component</span>
</code></pre>
<p>The <code>login-component.html</code> is going to contain the code below:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"login-block"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"row"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"col-md-12"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-lg btn-google btn-block text-uppercase btn-outline"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">img</span>
            <span class="hljs-attr">src</span>=<span class="hljs-string">"https://res.cloudinary.com/dz4tt9omp/image/upload/v1712537582/google-logo.png"</span>&gt;</span> Signup Using Google<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
</code></pre>
<p>While the <code>login-component.css</code> will contain the code below:</p>
<pre><code class="lang-css"><span class="hljs-selector-class">.login-block</span> {
  <span class="hljs-attribute">width</span>: <span class="hljs-number">300px</span>;
  <span class="hljs-attribute">margin</span>: <span class="hljs-number">0</span> auto;
  <span class="hljs-attribute">display</span>:flex;
  <span class="hljs-attribute">justify-content</span>:center;
  <span class="hljs-attribute">align-items</span>:center;
  <span class="hljs-attribute">height</span>:<span class="hljs-number">100vh</span>;
}

<span class="hljs-selector-class">.btn</span> {
  <span class="hljs-attribute">border-radius</span>: <span class="hljs-number">2px</span>;
  <span class="hljs-attribute">text-transform</span>: capitalize;
  <span class="hljs-attribute">font-size</span>: <span class="hljs-number">15px</span>;
  <span class="hljs-attribute">padding</span>: <span class="hljs-number">10px</span> <span class="hljs-number">19px</span>;
  <span class="hljs-attribute">cursor</span>: pointer
}


<span class="hljs-selector-class">.btn-google</span> {
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#545454</span>;
  <span class="hljs-attribute">background-color</span>: <span class="hljs-number">#ffffff</span>;
  <span class="hljs-attribute">box-shadow</span>: <span class="hljs-number">0</span> <span class="hljs-number">1px</span> <span class="hljs-number">2px</span> <span class="hljs-number">1px</span> <span class="hljs-number">#ddd</span>;
}
</code></pre>
<p>To see how the user interface looks, you can call the <code>&lt;app-login /&gt;</code> tag with the <code>app.component.html</code> file, since the route navigations have not yet been configured. The user interface should look like the screenshot below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1746232060227/98f18b59-d433-486b-adbe-f28302e8d901.png" alt="Screenshot of a web page running on localhost at port 4200. The page displays a centered white background with a single button labeled &quot;SIGNUP USING GOOGLE&quot; that includes a Google logo icon. The button is slightly elevated with a shadow effect." class="image--center mx-auto" width="1920" height="951" loading="lazy"></p>
<h2 id="heading-how-to-set-up-a-new-supabase-project">How to Set Up a New Supabase Project</h2>
<p>To set up Supabase, you will need to create a new account on <a target="_blank" href="https://supabase.com/">Supabase.com</a> by using either a GitHub account, or the traditional email and password. Once you’ve done this, you will be presented with a form to create a new organization as you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747178536860/972076bf-ae5e-4b33-b18c-011e1e63b3a0.png" alt="Supabase form to create a new organization with name, type, and plan fields." class="image--center mx-auto" width="1920" height="892" loading="lazy"></p>
<p>The organization will be created as fast as your internet speed. Once that is done, the next form you’ll see will allow you to create a new Supabase project.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747180463389/a41cd54d-4514-4a8b-8230-a348723b0a2f.png" alt="Supabase interface for creating a new project, showing fields for organization, project name, database password, and region selection." class="image--center mx-auto" width="1910" height="890" loading="lazy"></p>
<p>As you can see from the image above, all you need to do to create a new project is to set a database password and select a region close to where you think most of your users will be. This will help reduce latency. With that you can now click on the create button to create a new project.</p>
<p>Once the project creation is complete, you will be navigated to the dashboard below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747180885966/b9b89775-36cb-49e8-a596-c5ae4bad75a7.png" alt="Supabase dashboard showing a new project with no tables and a task list in progress." class="image--center mx-auto" width="1920" height="891" loading="lazy"></p>
<p>With that, you have now set up your new Supabase project.</p>
<h2 id="heading-how-to-set-up-google-oauth-20-for-authentication-and-authorization">How to Set Up Google OAuth 2.0 for Authentication and Authorization</h2>
<p>To set up Google OAuth 2.0, you need to create an account on <a target="_blank" href="https://console.cloud.google.com">Google Cloud Console</a>. Once you create an account, you will be navigated to the dashboard, where you can create a new project by clicking on the select project button on the top left-hand side of the dashboard.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747353793461/b42cd8f9-5a6c-4579-be15-074f9bf90e13.png" alt="Google Cloud console welcome page showing a $300 free credit offer. A 'Select a project' button is highlighted near the top." class="image--center mx-auto" width="1912" height="932" loading="lazy"></p>
<p>Once you’ve selected the newly created project, you can now begin implementing Google OAuth 2.0 by following these steps:</p>
<ul>
<li><p>Click on the hamburger menu on the left-hand side of the dashboard and hover over <strong>APIs and services.</strong></p>
</li>
<li><p>Click on <strong>Credentials</strong>, on the Credentials page, select <strong>Create Credentials</strong> at the top menu of the dashboard. A dropdown menu will appear. Select <strong>Create OAuth client ID</strong>.</p>
</li>
<li><p>On the Client ID page, you’ll get a warning message that says “To create an OAuth client ID, you must first configure your consent screen.” Click on the <strong>Configure consent screen</strong> button.</p>
</li>
</ul>
<p>Next, you’ll be directed to the Branding page. Click on the getting Started button, and you’ll be presented with a form on the overview page as you can see below. Then just fill out the form:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747358499033/47c07eda-9b1e-45ce-ae0b-bb478178a0a2.png" alt="Google Cloud console showing the 'Create branding' page under the Google Auth Platform. The user is filling out app information, including app name and support email, as part of the project configuration steps." class="image--center mx-auto" width="1916" height="885" loading="lazy"></p>
<p>You can now create the OAuth Consent Screen by heading to the Clients tab on the left-side of the dashboard and filling out the details for your application type, the name of your OAuth 2.0 client, as well as the Authorized JavaScript origins.</p>
<p>For the Authorized JavaScript origins, you can enter the URL (<a target="_blank" href="http://localhost:4200">http://localhost:4200</a>), since that is the development URL for our Angular application. Then click on the create button. You may get a warning saying “Note: It may take five minutes to a few hours for settings to take effect.”</p>
<p>Once the configuration is complete, you will get a modal that contains a Client ID and a Client Secret, as you can see below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747361106624/6ba6ed98-469a-4154-9d47-6719d982176e.png" alt="A Google Cloud OAuth client creation dialog displays the Client ID, Client secret, creation date, status as enabled, and a warning about downloading credentials before June 2025." class="image--center mx-auto" width="1918" height="884" loading="lazy"></p>
<p>Make sure you copy the Client ID and Client secret, as you will use this in the Supabase dashboard.</p>
<p>To complete the authentication and authorization setup, head to the Supabase dashboard. Then navigate to the Authentication menu, which is located in the items on the left-side of the dashboard. On this part of the dashboard, you will select <strong>Sign In / Providers</strong>.</p>
<p>On the Sign In / Providers page, scroll down to the <strong>Auth Providers</strong>, then select and enable <strong>Google</strong>. This is where you will paste in the credentials of the Client ID and Client Secret created on the <strong>Google Cloud Console</strong>. Then click on the save button – and make sure you copy the Callback URL (for OAuth).</p>
<p>The final step in this process is to head back to the GCP dashboard, and under the Clients tab, click on the edit icon of the OAuth 2.0 Client IDs you created previously.</p>
<p>Under the Authorized redirect URIs, click on the Add URI button. An input box will appear. Paste in the link of the Callback URL (for OAuth) you grabbed in the Supabase dashboard and click save.</p>
<h2 id="heading-how-to-configure-the-router-of-the-angular-application">How to Configure the Router of the Angular Application</h2>
<p>Earlier in this tutorial, you created two components: Chat and Login. At this point, you need to setup the route configuration in the <code>app.routes.ts</code>. In this file, add the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> routes: Routes = [
  {
    path: <span class="hljs-string">'chat'</span>,
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/chat/chat-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.ChatComponent),
  },
  {
    path: <span class="hljs-string">'login'</span>,
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/login/login-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.LoginComponent),
  },
  {
    path: <span class="hljs-string">''</span>,
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/login/login-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.LoginComponent),
  },
];
</code></pre>
<p>Above, you can see the two components now have their separate routes called <strong>chat</strong> and <strong>login</strong>, respectively. They can be accessed anywhere in the application.</p>
<h2 id="heading-how-to-set-up-the-authentication-service">How to Set Up the Authentication Service</h2>
<p>To setup the authentication service in the Angular application, use the following command:</p>
<pre><code class="lang-bash">ng g s services/auth-service
</code></pre>
<p>Next, you’ll generate the environments folders to setup the environment variables using the below command:</p>
<pre><code class="lang-bash">ng g environments
</code></pre>
<p>The final configuration you need to do from the terminal before you begin creating the function for the Angular authentication service is to install Supabase with the command below:</p>
<pre><code class="lang-bash">npm i @supabase/supabase-js
</code></pre>
<p>And with that, you now have Supabase installed in the project and you can begin integrating the functions in the service. Start from the <code>environment.development.ts</code> file<strong>.</strong> The current structure of this file should look this way by default:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> environment = {};
</code></pre>
<p>To configure this file, you need to head to the Supabase dashboard. Locate and select the settings menu on the left hand panel of the dashboard. Under the <strong>Configuration</strong> tab, click on Data API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1747569518560/94ae00c7-4f62-4cc7-a0f9-250088754acb.png" alt="Screenshot of Supabase API settings, showing the project URL and API keys section with arrows pointing to the URL and keys, plus buttons to copy each credential." class="image--center mx-auto" width="1918" height="886" loading="lazy"></p>
<p>You can now grab both the Project URL and anon public key (the arrow is pointing to it in the image above). You can now head over to the <code>environment.development.ts</code> file and paste in the values of the copied link following the format below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> environment = {
  production: <span class="hljs-literal">false</span>,
  supabaseUrl: <span class="hljs-string">'https://zktqzszvllbxvjfzkhvk.supabase.co'</span>,
  supabaseKey:
    <span class="hljs-string">'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InprdHF6c3p2bGxieHZqZnpraHZrIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDcyNTg3MDgsImV4cCI6MjA2MjgzNDcwOH0.qf3MA-La6se8QijzLFALKc_XdiISmzDk7AZw4-na0uA'</span>,
};
</code></pre>
<p>With the environment variables all in place, you can now create the functions for the authentication service.</p>
<p>In the <code>auth-service.ts</code> which you created previously, start by importing the Supabase package as well as the environments file as you can see below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;
</code></pre>
<p>Next, complete the injection of the Supabase <code>npm</code> package by injecting it into your constructor:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthService {
  supabase!: SupabaseClient;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
      <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );
  }
}
</code></pre>
<h3 id="heading-how-to-create-the-service-functions-for-login-and-sign-out-functionality">How to Create the Service Functions for Login and Sign Out Functionality</h3>
<p>The final step in setting up the <code>Auth</code>service is to create the functions which will later be called in the template. You are going to create four functions which you can see in the code below:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">private</span> router = inject(Router);
  <span class="hljs-keyword">private</span> _ngZone = inject(NgZone);

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );

    <span class="hljs-built_in">this</span>.supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {

      <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'session'</span>, <span class="hljs-built_in">JSON</span>.stringify(session?.user));

      <span class="hljs-keyword">if</span> (session?.user) {
        <span class="hljs-built_in">this</span>._ngZone.run(<span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">this</span>.router.navigate([<span class="hljs-string">'/chat'</span>]);
        });
      }
    });
  }

  get isLoggedIn(): <span class="hljs-built_in">boolean</span> {
    <span class="hljs-keyword">const</span> user = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'session'</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">return</span> user === <span class="hljs-string">'undefined'</span> ? <span class="hljs-literal">false</span> : <span class="hljs-literal">true</span>;
  }

  <span class="hljs-keyword">async</span> signInWithGoogle() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.auth.signInWithOAuth({
      provider: <span class="hljs-string">'google'</span>,
    });
  }

  <span class="hljs-keyword">async</span> signOut() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.auth.signOut();
  }
}
</code></pre>
<p>The first function created is within the constructor. This is the <code>onAuthStateChange</code> callback function which is derived from Supabase and allows us to listen to Auth changes. It accepts two parameters called <code>event</code> and <code>session</code>.</p>
<p>Here, two conditions were instantiated within the <code>onAuthStateChange</code> callback function. They say that when the <code>session?.user</code> exists, you proceed to set the value to the local storage, and then navigate the user to the dashboard using the Angular router (which has been imported and injected using the <code>inject()</code> function).</p>
<p>The second function, <code>isLoggedIn()</code>, is a getter function that returns a Boolean. It returns either true or false, depending on if it is able to retrieve the user session from <code>localStorage</code>. This function will be used in the authentication guard which you’ll create later.</p>
<p>The third function, <code>signInWithGoogle()</code>, allows the user log into the dashboard using the <code>signInWithOAuth()</code> method provided by Supabase. This allows the user to log into the dashboard using a Google Gmail account.</p>
<p>The final function, <code>signOut()</code>, allows users to logout of the dashboard by resetting the state of the user session to null.</p>
<p>With all these functions created, the final code base in the <code>auth-service.ts</code> should look like this:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable, NgZone, inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;
<span class="hljs-keyword">import</span> { Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> AuthService {
  <span class="hljs-keyword">private</span> supabase!: SupabaseClient;

  <span class="hljs-keyword">private</span> router = inject(Router);
  <span class="hljs-keyword">private</span> _ngZone = inject(NgZone);
  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );

    <span class="hljs-built_in">this</span>.supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'event'</span>, event);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'session'</span>, session);

      <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'session'</span>, <span class="hljs-built_in">JSON</span>.stringify(session?.user));

      <span class="hljs-keyword">if</span> (session?.user) {
        <span class="hljs-built_in">this</span>._ngZone.run(<span class="hljs-function">() =&gt;</span> {
          <span class="hljs-built_in">this</span>.router.navigate([<span class="hljs-string">'/chat'</span>]);
        });
      }
    });
  }

  get isLoggedIn(): <span class="hljs-built_in">boolean</span> {
    <span class="hljs-keyword">const</span> user = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'session'</span>) <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;

    <span class="hljs-keyword">return</span> user === <span class="hljs-string">'undefined'</span> ? <span class="hljs-literal">false</span> : <span class="hljs-literal">true</span>;
  }

  <span class="hljs-keyword">async</span> signInWithGoogle() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.auth.signInWithOAuth({
      provider: <span class="hljs-string">'google'</span>,
    });
  }

  <span class="hljs-keyword">async</span> signOut() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.auth.signOut();
  }
}
</code></pre>
<p>You can now utilize these functions anywhere in the Angular project as a form of state management.</p>
<h3 id="heading-how-to-integrate-the-authentication-service-function-in-the-template">How to Integrate the Authentication Service Function in the Template</h3>
<p>The first function we’ll use is the <code>signInWithGoogle()</code> function. We’ll use it in the <code>login-component.ts</code> file to allow users log into the application as you can see below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/auth-service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-login'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [],
  templateUrl: <span class="hljs-string">'./login-component.html'</span>,
  styleUrl: <span class="hljs-string">'./login-component.css'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> LoginComponent {
  <span class="hljs-keyword">private</span> auth = inject(AuthService);

  <span class="hljs-keyword">async</span> handleAuth() {
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.auth.signInWithGoogle();
  }
}
</code></pre>
<p>Above, you implemented three features:</p>
<ul>
<li><p>Importing <code>AuthService</code> into the <code>LoginComponent</code></p>
</li>
<li><p>Injecting <code>AuthService</code> using the Inject function into the <code>LoginComponent</code></p>
</li>
<li><p>Creating the <code>handleAuth()</code> function that allows you call the <code>signInWithGoogle()</code> from the <code>AuthService</code> file.</p>
</li>
</ul>
<p>Now you can head to the <code>login-component.html</code> file and call the <code>handleAuth(</code>) function as below within the <code>&lt;a&gt;</code> tag:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">section</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"login-block"</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>

    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"row"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"col-md-12"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"handleAuth()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-lg btn-google btn-block text-uppercase btn-outline"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span>&gt;</span><span class="hljs-tag">&lt;<span class="hljs-name">img</span>
            <span class="hljs-attr">src</span>=<span class="hljs-string">"https://res.cloudinary.com/dz4tt9omp/image/upload/v1712537582/google-logo.png"</span>&gt;</span> Signup Using Google<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">section</span>&gt;</span>
</code></pre>
<p>Before you test the implementation, you will need to set the URL configuration in the Supabase dashboard. The URL configuration allows the URLs that authentication providers permit to redirect and post authentication, including wildcards.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748363896805/0b8f2ed1-ba45-44b9-93b8-0f470cb5ff32.png" alt="Interface displaying authentication settings, including site URL configuration and allowed redirect URLs for a web application." class="image--center mx-auto" width="1920" height="886" loading="lazy"></p>
<p>As you can see in the above image, the two Redirect URLs provided are localhost, since we are still currently creating the app in our local machine.</p>
<p>With this, you can test the Google OAuth 2.0 configuration by typing the localhost URL (<a target="_blank" href="http://localhost:4200/">http://localhost:4200</a>) in the browser, clicking on the <strong>Signup Using Google</strong> button<strong>,</strong> and selecting a Gmail account you want to sign up/login with. Then you should get navigated to the Chat component.</p>
<h2 id="heading-how-to-create-route-protection-in-angular">How to Create Route Protection in Angular</h2>
<p>To create route protection in Angular, you can use an in-built mechanism called a <strong>Route Guard</strong>. The Route Guard is used to control access to certain parts of the Angular application using certain conditions before a route is activated or accessible to the user.</p>
<p>In our case, you will be generating the Route Guard as a function (which is the default in our current version of Angular (20), instead of as a class) using the command below:</p>
<pre><code class="lang-bash">ng generate guard auth-guard
</code></pre>
<p>You will then see this prompt that asks “Which type of guard would you like to create?”:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748365560338/650c5bf7-8e1c-45cf-915b-c988c167416a.png" alt="650c5bf7-8e1c-45cf-915b-c988c167416a" class="image--center mx-auto" width="1872" height="379" loading="lazy"></p>
<p>Use the spacebar to select <code>CanActivate</code>, and then press the Enter key to generate the Guard. Two files will be generated: the <code>auth-guard.spec.ts</code> file (for testing), and the <code>auth-guard.ts</code> file. Within the <code>auth-guard.ts</code> file, you will see the boilerplate code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CanActivateFn } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> authGuard: CanActivateFn = <span class="hljs-function">(<span class="hljs-params">route, state</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
};
</code></pre>
<p>You can start modifying the above template by importing the Angular Router, the <code>AuthService</code> file that you created earlier, as well as the Inject function:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CanActivateFn, Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./services/auth-service'</span>;
<span class="hljs-keyword">import</span> { inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
</code></pre>
<p>Next, use the <code>isLoggedIn</code> getter that you created earlier in the <code>AuthService</code> file (which returns a Boolean) to conditionally activate the Chat dashboard for the user based on their login status using the code below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { CanActivateFn, Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'./services/auth-service'</span>;
<span class="hljs-keyword">import</span> { inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> authGuard: CanActivateFn = <span class="hljs-function">(<span class="hljs-params">route, state</span>) =&gt;</span> {
  <span class="hljs-keyword">if</span> (inject(AuthService).isLoggedIn === <span class="hljs-literal">false</span>) {
    inject(Router).navigate([<span class="hljs-string">'/login'</span>]);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  }
};
</code></pre>
<p>To complete the Guard integration, head over to the <code>app.routes.ts</code> file and import and inject the Authentication Guard as you can see below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Routes } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;
<span class="hljs-keyword">import</span> { authGuard } <span class="hljs-keyword">from</span> <span class="hljs-string">'./auth-guard'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> routes: Routes = [
  {
    path: <span class="hljs-string">'chat'</span>,
    canActivate: [authGuard],
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/chat/chat-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.ChatComponent),
  },
  {
    path: <span class="hljs-string">'login'</span>,
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/login/login-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.LoginComponent),
  },
  {
    path: <span class="hljs-string">''</span>,
    loadComponent: <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">import</span>(<span class="hljs-string">'./pages/login/login-component'</span>).then(<span class="hljs-function">(<span class="hljs-params">com</span>) =&gt;</span> com.LoginComponent),
  },
];
</code></pre>
<p>With this, the route protection implementation is now complete and only authenticated users can view the dashboard.</p>
<h2 id="heading-how-to-create-and-setup-the-users-table-in-supabase-using-the-sql-editor">How to Create and Setup the Users Table in Supabase using the SQL Editor</h2>
<p>To create and setup the users table, use the schema below:</p>
<ul>
<li><p>id (uuid)</p>
</li>
<li><p>full_name (text)</p>
</li>
<li><p>avatar_url (text)</p>
</li>
</ul>
<p>You can use the <strong>SQL</strong> Editor in Supabase. The SQL Editor is the third item on the menu panel in the Supabase dashboard. Here you are going to type in the query below in the SQL Editor input field:</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-built_in">public</span>.users (
   id <span class="hljs-type">uuid</span> <span class="hljs-keyword">not</span> <span class="hljs-keyword">null</span> <span class="hljs-keyword">references</span> auth.users <span class="hljs-keyword">on</span> <span class="hljs-keyword">delete</span> <span class="hljs-keyword">cascade</span>,
   full_name <span class="hljs-type">text</span> <span class="hljs-keyword">NULL</span>,
   avatar_url <span class="hljs-type">text</span> <span class="hljs-keyword">NULL</span>,
   <span class="hljs-keyword">primary key</span> (id)
);
</code></pre>
<p>You can now click on the Run button on the bottom right. You should get a message that says: <strong>Success. No rows returned</strong>, as you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748394167618/5db26c97-a947-4233-8232-3aba72187a12.png" alt="SQL code for creating a user profile table with fields for id, full name, and avatar URL. Returns No rows returned after execution." class="image--center mx-auto" width="1918" height="885" loading="lazy"></p>
<p>Now let’s go ahead and enable row level security, as well as the Supabase function and trigger.</p>
<h3 id="heading-how-to-configure-row-level-security-policies-in-supabase-with-the-sql-editor">How to Configure Row Level Security Policies in Supabase with the SQL Editor</h3>
<p>Row Level Security (RLS) in Supabase allows you to control access to individual rows in your database tables based on custom logic. It’s one of the core features for building secure, multi-user applications with Supabase.</p>
<p>RLS lets you define SQL policies that determine which users can <code>SELECT</code>, <code>INSERT</code>, <code>UPDATE</code>, or <code>DELETE</code> specific rows in a table.</p>
<p>To enable RLS in the <strong>users</strong> table, type the command below in your SQL Editor:</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-built_in">public</span>.users <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>For the purpose of this tutorial, you are going to create just two policies, which are:</p>
<ol>
<li><p>The ability for users to access their own profile</p>
</li>
<li><p>The ability for users to update their own profile</p>
</li>
</ol>
<h4 id="heading-the-ability-for-users-to-access-their-own-profile">The ability for users to access their own profile</h4>
<p>To enable users access their own profile, head back to the SQL editor and create a new snippet with the following query:</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> "Permit Users to Access Their Profile"
  <span class="hljs-keyword">ON</span> <span class="hljs-built_in">public</span>.users
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">SELECT</span>
  <span class="hljs-keyword">USING</span> ( auth.uid() = id );
</code></pre>
<p>With this query, users will be able to access their own profile as long as the authenticated user’s ID matches the <code>id</code> of the column of the row.</p>
<h4 id="heading-the-ability-for-users-to-update-their-own-profile">The ability for users to update their own profile</h4>
<p>To enable users update their own profile, head back to the SQL editor and create a new snippet with the following query:</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> "Permit Users to Update Their Profile"
  <span class="hljs-keyword">ON</span> <span class="hljs-built_in">public</span>.users
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">UPDATE</span>
  <span class="hljs-keyword">USING</span> ( auth.uid() = id );
</code></pre>
<p>With the above query, users will be able to update their own profile as long as the authenticated user’s ID matches the <code>id</code> of the column of the row.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748397263186/11e886f4-0d08-4907-9859-9269ee0da2ef.png" alt="Screenshot of SQL Editor displaying a policy script for user profile updates, with navigation pane and no results returned." class="image--center mx-auto" width="1910" height="886" loading="lazy"></p>
<h3 id="heading-how-to-configure-supabase-functions-in-supabase-with-the-sql-editor">How to Configure Supabase Functions in Supabase with the SQL Editor</h3>
<p><strong>Supabase Functions</strong> are serverless functions that can be deployed and run within your Supabase project using <strong>Supabase Edge Functions</strong>.</p>
<p>In this project, you will create a trigger function that automatically creates a new row in the users table whenever a new user is created in the <code>auth.users</code> table.</p>
<pre><code class="lang-pgsql"><span class="hljs-keyword">CREATE</span>
<span class="hljs-keyword">OR REPLACE</span> <span class="hljs-keyword">FUNCTION</span> <span class="hljs-built_in">public</span>.user_profile() <span class="hljs-keyword">RETURNS</span> <span class="hljs-type">TRIGGER</span> <span class="hljs-keyword">AS</span> $$<span class="pgsql"> <span class="hljs-keyword">BEGIN</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">INTO</span> <span class="hljs-built_in">public</span>.users (id, full_name,avatar_url)
<span class="hljs-keyword">VALUES</span>
  (
    <span class="hljs-built_in">NEW</span>.id,
    <span class="hljs-built_in">NEW</span>.raw_user_meta_data -&gt;&gt; <span class="hljs-string">'full_name'</span>::<span class="hljs-type">TEXT</span>,
    <span class="hljs-built_in">NEW</span>.raw_user_meta_data -&gt;&gt; <span class="hljs-string">'avatar_url'</span>::<span class="hljs-type">TEXT</span>
  );
<span class="hljs-keyword">RETURN</span> <span class="hljs-built_in">NEW</span>;
<span class="hljs-keyword">END</span>;
$$</span> <span class="hljs-keyword">LANGUAGE</span> plpgsql <span class="hljs-keyword">SECURITY</span> <span class="hljs-keyword">DEFINER</span>;
</code></pre>
<p>To summarize the above query:</p>
<ul>
<li><p>You start by defining or replacing a function named <code>user_profile()</code> that will be used as a trigger.</p>
</li>
<li><p>Next, the trigger inserts a new row into the <code>public.users</code> table, and then extracts the <code>full_name</code> and <code>avatar_url</code> from the user's metadata as text.</p>
</li>
<li><p>The inserted record is now returned when the trigger function is complete</p>
</li>
<li><p>Finally, you use the <code>SECURITY DEFINER</code> keyword so that the function can run with the privileges of the user who created it.</p>
</li>
</ul>
<h3 id="heading-how-to-configure-supabase-trigger-in-supabase-with-the-sql-editor">How to Configure Supabase Trigger in Supabase with the SQL Editor</h3>
<p>A trigger in Supabase is a PostgreSQL feature used to automatically run a function in response to events on a table (SELECT, INSERT, UPDATE, or DELETE). It’s mostly used with Row Level Security or syncing data across tables.</p>
<p>In this project, you will create a Supabase trigger that automatically runs a function after a new user is created in the <code>auth.users</code> table.</p>
<pre><code class="lang-pgsql"> <span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span>
  create_user_trigger
  <span class="hljs-keyword">AFTER</span> <span class="hljs-keyword">INSERT</span> <span class="hljs-keyword">ON</span> auth.users
  <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">PROCEDURE</span>
    <span class="hljs-built_in">public</span>.user_profile();
</code></pre>
<p>To summarize the above query:</p>
<ul>
<li><p>The first line creates a <strong>trigger</strong> named <code>create_user_trigger</code>.</p>
</li>
<li><p>Next, the INSERT ON statement is activated when a user signs up and a new row is inserted into the <code>auth.users</code> table</p>
</li>
<li><p>Then the trigger runs once for every new user added in a new row.</p>
</li>
<li><p>Finally, the custom function <code>public.user_profile()</code> is called to perform some logic, typically inserting data into the <code>users</code> table.</p>
</li>
</ul>
<p>With the above integration, you can now log into the dashboard with a new google account and view the users table. There you will see the data that contains the <strong>id</strong>, <strong>full_name</strong>, and <strong>avatar_url</strong> as you can see below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748560146471/4c7663a8-6708-461a-8ed5-7ed8a3931318.png" alt="Screenshot of a database table editor displaying user data, including name and avatar URL, with options for filtering and sorting." class="image--center mx-auto" width="1920" height="878" loading="lazy"></p>
<h2 id="heading-how-to-create-and-setup-the-chat-table-in-supabase-using-the-user-interface">How to Create and Setup the Chat Table in Supabase using the User Interface</h2>
<p>To create the chat table, you will use the user interface in Supabase instead of the SQL Editor. To do this, you need to head to the Table Editor menu on the dashboard and click on the <strong>New Table</strong> button.</p>
<p>Once selected, a modal will popup which contains some input fields such as the table name, description, and columns. You can call the table name chat and omit the description for now since it’s optional. In the columns section, fill out the fields using the schema below:</p>
<ul>
<li><p>id (uuid)</p>
</li>
<li><p>Created At (date)</p>
</li>
<li><p>text (text)</p>
</li>
<li><p>editable (boolean)</p>
</li>
<li><p>sender (uuid)</p>
</li>
</ul>
<p>You can see the configuration for this in the table below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748564800333/0e879684-75d3-4c47-9f38-56d6041d6b38.png" alt="Screenshot of a table editor interface displaying fields for creating a new database table with various data types and options." class="image--center mx-auto" width="1882" height="879" loading="lazy"></p>
<p>Next up, you need to add a foreign key relation for the users table. To do this, you scroll to the bottom of the modal and click on the <strong>Add foreign key relation</strong> button. This will prompt another modal on top of the current modal. Here you can take the following steps:</p>
<ul>
<li><p>Under the <strong>Select a table to reference to</strong> label, select the <strong>users</strong> table<strong>.</strong></p>
</li>
<li><p>Under the <strong>public.chat</strong> label, select the <strong>sender</strong> option.</p>
</li>
<li><p>Under public.users label, select <strong>uuid.</strong></p>
</li>
<li><p>Under the <strong>Action if referenced row is updated</strong> label, select <strong>Cascade</strong>.</p>
</li>
<li><p>Under the <strong>Action if referenced row is removed</strong> label, select <strong>Cascade</strong> as well.</p>
</li>
</ul>
<p>If you’ve followed the above steps, you can now click on the save button, which successfully creates the chat table.</p>
<h2 id="heading-how-to-create-and-setup-the-chat-table-policies-in-supabase">How to Create and Setup the Chat Table Policies in Supabase</h2>
<p>The final step you need to perform for the chat table is to add a Row Level Security policy. You can do this by clicking the <strong>Add RLS policy</strong> button at the top of the chat table page.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748567233239/6f69e305-5e09-4485-89b7-0115d41c8e0f.png" alt="A web interface displaying a Table Editor with options for managing chat and user data in a database schema." class="image--center mx-auto" width="1915" height="385" loading="lazy"></p>
<p>A new page will appear. Then you can click on the <strong>Create policy</strong> button, which displays a modal.</p>
<p>The first policy you will create is the <strong>DELETE</strong> policy, which will have the configuration you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748779442105/f977dafb-ab73-4bb7-8643-9aa4e889c359.png" alt="Screenshot of a database policy settings interface for deleting user records based on user ID in a chat application." class="image--center mx-auto" width="1920" height="777" loading="lazy"></p>
<p>From the above image, we made these four implementations:</p>
<ul>
<li><p>First, we entered the policy name as “<strong>Delete by User ID</strong>“.</p>
</li>
<li><p>Next we selected the <strong>DELETE</strong> policy command clause.</p>
</li>
<li><p>Then under the targeted roles, we selected authenticated in the drop down select, to allow only authenticated users to perform delete operations.</p>
</li>
<li><p>Finally, under the <strong>USE OPTIONS ABOVE TO EDIT</strong> section, in line 7, we condition the query as <code>(auth.uid() = sender)</code> This allows only logged in users to delete their data.</p>
</li>
</ul>
<p>You can now click on the <strong>Save policy</strong> button to complete the DELETE setup.</p>
<p>The second policy you will create is the <strong>INSERT</strong> policy, which will have the configuration you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748823531256/14aea41d-f221-42df-a739-b40a680395e3.png" alt="A policy configuration interface for inserting records in a chat table, targeting authenticated users with specific criteria." class="image--center mx-auto" width="1900" height="879" loading="lazy"></p>
<p>From the above image, four implementations were made:</p>
<ul>
<li><p>First, we entered the policy name as “<strong>Insert for Authenticated Users</strong>“.</p>
</li>
<li><p>Next we selected the <strong>INSERT</strong> policy command clause.</p>
</li>
<li><p>Then under the targeted roles, we selected authenticated in the drop down to allow only authenticated users perform insert operations.</p>
</li>
<li><p>Finally, under the <strong>USE OPTIONS ABOVE TO EDIT</strong> section, in line 7, the query was conditioned as <code>((sender = auth.uid()) AND (created_at = now()))</code>. The first condition ensures that the <code>sender</code> field in the inserted row matches the currently logged-in user's ID (from the Supabase JWT), while the second condition ensures that the <code>created_at</code> field is exactly equal to the current timestamp at the time of insertion.</p>
</li>
</ul>
<p>The third policy you will create is the <strong>SELECT</strong> policy, which will have the configuration you can see in the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1748824129598/8c15b701-dadb-4763-b5eb-e9484bd84470.png" alt="Row-level security policy configuration for a database table, specifying access permissions for authenticated users based on SELECT criteria." class="image--center mx-auto" width="1904" height="882" loading="lazy"></p>
<p>From the above image, we implemented four things:</p>
<ul>
<li><p>First, we entered the policy name as “<strong>Read Data for Authenticated Users</strong>“.</p>
</li>
<li><p>Next we selected the <strong>SELECT</strong> policy command clause, <em>pun intended</em> <strong>😊*</strong>.*</p>
</li>
<li><p>Then under the targeted roles, we selected authenticated in the drop down to allow only authenticated users perform select operations.</p>
</li>
<li><p>Finally, under the <strong>USE OPTIONS ABOVE TO EDIT</strong> section, in line 7, the query was conditioned as <code>true</code>. This allows all authenticated users to read all rows, or chats in our case.</p>
</li>
</ul>
<p>With the above implementation, you’ve created all the policies needed for the chat application.</p>
<h2 id="heading-how-to-integrate-functionality-to-create-a-new-chat-message-in-the-angular-application">How to Integrate Functionality to Create a New Chat Message in the Angular Application</h2>
<p>Now let’s add the code that lets users create a new chat message. First, start by creating a new Angular service using the command below:</p>
<pre><code class="lang-powershell">ng g s services/chat<span class="hljs-literal">-service</span>
</code></pre>
<p>Within the <code>chat-service.ts</code> file, you can now configure the Supabase client, just as we did in the <code>auth-service.ts</code> file as seen below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable, signal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatService {
  supabase!: SupabaseClient;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );
  }
}
</code></pre>
<p>Next, create the function that enables you to create a new chat message. The function called <code>chatMessage()</code> is below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable, signal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatService {
  supabase!: SupabaseClient;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );
  }

  <span class="hljs-keyword">async</span> chatMessage(text: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.from(<span class="hljs-string">'chat'</span>).insert({ text });

      <span class="hljs-keyword">if</span> (error) {
        alert(error.message);
      }
    } <span class="hljs-keyword">catch</span> (error) {
      alert(error);
    }
  }
}
</code></pre>
<p>The above <code>chatMessage</code> function sends a chat message by inserting it into the <code>chat</code> table in Supabase.</p>
<p>You can now call this service in the <code>chat-component.ts</code> file. Within the <code>chat-component.ts</code>, import and inject the <code>chat-service.ts</code> file.</p>
<p>To send the data to the Supabase database, you need to setup Reactive form. Reactive form in Angular enables you to get data from an input field, which can be passed as a payload and then inserted into the database.</p>
<p>To setup a Reactive form in Angular, follow these steps:</p>
<ul>
<li><p>Import <code>FormBuilder</code>, <code>FormGroup</code>, <code>ReactiveFormsModule</code>, and <code>Validators</code> from <code>@angular/forms</code></p>
</li>
<li><p>Insert the <code>ReactiveFormsModule</code> inside of the imports array.</p>
</li>
<li><p>Inject the <code>FormBuilder</code> as a variable.</p>
</li>
<li><p>Declare a property that will hold the <strong>Reactive Form group.</strong></p>
</li>
<li><p>Inject the <code>FormBuilder</code> into the <code>ngOnInit</code> lifecycle hook.</p>
</li>
</ul>
<p>The code for the Reactive form setup is below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/auth-service'</span>;
<span class="hljs-keyword">import</span> {
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-chat'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [ReactiveFormsModule],
  templateUrl: <span class="hljs-string">'./chat-component.html'</span>,
  styleUrl: <span class="hljs-string">'./chat-component.css'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatComponent {
  chatForm!: FormGroup;
  <span class="hljs-keyword">private</span> fb = inject(FormBuilder);

  ngOnInit() {
    <span class="hljs-built_in">this</span>.chatForm = <span class="hljs-built_in">this</span>.fb.group({
      chat_message: [<span class="hljs-string">''</span>, Validators.required],
    });
  }
}
</code></pre>
<p>To complete the Reactive form setup, bind the <code>FormGroup</code> into the HTML file. Also bind the disabled attribute, which disables the button when the form is invalid, as you can see below:</p>
<pre><code class="lang-xml">  <span class="hljs-tag">&lt;<span class="hljs-name">form</span> [<span class="hljs-attr">formGroup</span>]=<span class="hljs-string">"chatForm"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-grow-0 py-3 px-4 border-top"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input-group"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">formControlName</span>=<span class="hljs-string">"chat_message"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Type your message"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> [<span class="hljs-attr">disabled</span>]=<span class="hljs-string">"!chatForm.valid"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;</span>Send<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
           <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
</code></pre>
<p>With the Reactive form setup complete, you can now create the function that calls the service which allows you create a new chat message.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, inject } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/auth-service'</span>;
<span class="hljs-keyword">import</span> {
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;
<span class="hljs-keyword">import</span> { ChatService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/chat-service'</span>;

<span class="hljs-meta">@Component</span>({
  selector: <span class="hljs-string">'app-chat'</span>,
  standalone: <span class="hljs-literal">true</span>,
  imports: [ReactiveFormsModule],
  templateUrl: <span class="hljs-string">'./chat-component.html'</span>,
  styleUrl: <span class="hljs-string">'./chat-component.css'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatComponent {
  <span class="hljs-keyword">private</span> chat_service = inject(ChatService);
  chatForm!: FormGroup;
  <span class="hljs-keyword">private</span> fb = inject(FormBuilder);

  ngOnInit() {
    <span class="hljs-built_in">this</span>.chatForm = <span class="hljs-built_in">this</span>.fb.group({
      chat_message: [<span class="hljs-string">''</span>, Validators.required],
    });
  }

  onSubmit() {
    <span class="hljs-keyword">const</span> formValue = <span class="hljs-built_in">this</span>.chatForm.value.chat_message;
    <span class="hljs-built_in">this</span>.chat_service
      .chatMessage(formValue)
      .then(<span class="hljs-function">(<span class="hljs-params">res</span>) =&gt;</span> {
        <span class="hljs-built_in">this</span>.chatForm.reset();
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        alert(err.message);
      });
  }
}
</code></pre>
<p>The <code>onSubmit()</code> function in the above code basically does the following tasks:</p>
<ul>
<li><p>Gets the data from the Reactive form input field using the variable called <code>formValue</code></p>
</li>
<li><p>Calls the <code>chatMessage()</code> method from the <code>ChatService</code>, passing the data from the input field.</p>
</li>
<li><p>If successful, it resets the form.</p>
</li>
<li><p>If there's an error, it shows an alert with the error message.</p>
</li>
</ul>
<p>In the <code>chat-component.html</code> file, use of the <code>(ngSubmit)</code> directive to bind the <code>onSubmit()</code> function to the form:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">form</span> [<span class="hljs-attr">formGroup</span>]=<span class="hljs-string">"chatForm"</span> (<span class="hljs-attr">ngSubmit</span>)=<span class="hljs-string">"onSubmit()"</span>&gt;</span>
</code></pre>
<p>You can now test to see if the data we send from the input field saves directly into the <strong>chat</strong> database table.</p>
<p><strong>NOTE:</strong> make sure you <strong>delete all current users saved in the users table and authentication page on Supabase</strong> before trying this out for best results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749042436895/a50416e7-3f8b-48a9-b8f3-96df520b2238.png" alt="Screenshot of a chat interface showing a message from &quot;Sharon Doe,&quot; with a timestamp and a text input field at the bottom." class="image--center mx-auto" width="1917" height="880" loading="lazy"></p>
<p>From the above image, you will click on the send button and send the <strong>Test</strong> data in the input field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749043485066/94b53998-e924-44c2-bbfd-e26d591fdf54.png" alt="Table editor interface displaying the &quot;chat&quot; table with columns including ID, created_at, text, editable, and sender, showing entries and configuration options." class="image--center mx-auto" width="1911" height="739" loading="lazy"></p>
<p>The data should now be successfully saved into the database and the <strong>INSERT</strong> operation should now be integrated into the Angular application.</p>
<h2 id="heading-how-to-fetch-data-in-the-angular-application-from-supabase">How to Fetch Data in the Angular Application from Supabase</h2>
<p>To fetch data from the chat table from Supabase, start by creating a service function in the <code>chat-service.ts</code> file, as seen below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Injectable, signal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { SupabaseClient, createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>;
<span class="hljs-keyword">import</span> { environment } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../environments/environment.development'</span>;

<span class="hljs-meta">@Injectable</span>({
  providedIn: <span class="hljs-string">'root'</span>,
})
<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> ChatService {
  supabase!: SupabaseClient;

  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.supabase = createClient(
      environment.supabaseUrl,
      environment.supabaseKey
    );
  }

  <span class="hljs-keyword">async</span> chatMessage(text: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.from(<span class="hljs-string">'chat'</span>).insert({ text });
      <span class="hljs-keyword">if</span> (error) {
        alert(error.message);
      }
    } <span class="hljs-keyword">catch</span> (error) {
      alert(error);
    }
  }

    <span class="hljs-keyword">async</span> listChat() {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase
        .from(<span class="hljs-string">'chat'</span>)
        .select(<span class="hljs-string">'*,users(*)'</span>);

      <span class="hljs-keyword">if</span> (error) {
        alert(error.message);
      }

      <span class="hljs-keyword">return</span> data;
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-keyword">throw</span> error;
    }
  }
}
</code></pre>
<p>To summarize the function above called <code>listChat()</code>:</p>
<ul>
<li><p>We fetch the chat messages from the <code>chat</code> table using the <code>from</code> clause.</p>
</li>
<li><p>Then we include the related user info by joining the users table with <code>(select(', users()'))</code>.</p>
</li>
<li><p>An alert message is shown if there's a Supabase error.</p>
</li>
<li><p>Finally, the fetched data is returned, an error is thrown if something goes wrong.</p>
</li>
</ul>
<p>Before you head to the <code>chat-component.ts</code> file to consume the <code>listChat()</code> service function, you need to create an interface which helps shape the structure of the array of objects returned from Supabase. This gives us type safety and consistency.</p>
<p>To set up the interface, create an <strong>interface</strong> folder within the <strong>app</strong> directory. Here you will create a file called <code>chat-response.ts</code>. Then create the structure below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Ichat {
  created_at: <span class="hljs-built_in">string</span>;
  editable: <span class="hljs-built_in">boolean</span>;
  id: <span class="hljs-built_in">string</span>;
  sender: <span class="hljs-built_in">string</span>;
  text: <span class="hljs-built_in">string</span>;
  users: {
    avatar_url: <span class="hljs-built_in">string</span>;
    id: <span class="hljs-built_in">string</span>;
    full_name: <span class="hljs-built_in">string</span>;
  };
}
</code></pre>
<p>Heading back to the <code>chat-component.ts</code>, import both the interface which was named <code>Ichat</code> as well as <code>signal</code> and <code>effect</code> from <code>@angular/core</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, effect, inject, signal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { AuthService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/auth-service'</span>;
<span class="hljs-keyword">import</span> {
  FormBuilder,
  FormGroup,
  ReactiveFormsModule,
  Validators,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/forms'</span>;
<span class="hljs-keyword">import</span> { ChatService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/chat-service'</span>;
<span class="hljs-keyword">import</span> { Ichat } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../interface/chat-response'</span>;
</code></pre>
<p>Next, create a variable called <code>chats</code>, which will hold the response from the Supabase client as a signal:</p>
<pre><code class="lang-typescript">  chats = signal&lt;Ichat[]&gt;([]);
</code></pre>
<p>With this, you can now create the function that fetches the chat array of objects from the Supabase dashboard:</p>
<pre><code class="lang-typescript">  onListChat() {
    <span class="hljs-built_in">this</span>.chat_service
      .listChat()
      .then(<span class="hljs-function">(<span class="hljs-params">res: Ichat[] | <span class="hljs-literal">null</span></span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(res);
        <span class="hljs-keyword">if</span> (res !== <span class="hljs-literal">null</span>) {
          <span class="hljs-built_in">this</span>.chats.set(res);
        } <span class="hljs-keyword">else</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'No messages Found'</span>);
        }
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        alert(err.message);
      });
  }
</code></pre>
<p>To summarize the function above, we started by:</p>
<ul>
<li><p>Calling the <code>listChat()</code> function from the <code>ChatService</code> to fetch the chat messages.</p>
</li>
<li><p>If messages are returned, it updates the chats signal with the result, by using the <code>set()</code> method derived from signals.</p>
</li>
<li><p>In the event where no messages are returned, it logs <code>"No messages Found"</code> to the console.</p>
</li>
<li><p>If an error occurs, it shows an alert with the error message.</p>
</li>
</ul>
<p>We then call the <code>onListChat()</code> function within the constructor using the <code>effect()</code> function, which helps handle asynchronous operations.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    effect(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-built_in">this</span>.onListChat();
    });
  }
</code></pre>
<p>When the application is saved, you can see the data in the console from the image below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749066359110/dedaa60a-c711-42d9-bac1-c9a130fcc4d1.png" alt="Screenshot of a chat application showing a message from Sharon Doe, along with developer tools displaying the message data in an array of object." class="image--center mx-auto" width="1912" height="868" loading="lazy"></p>
<p>You can now display the chat data in the HTML file of the page by getting rid of the placeholder text.</p>
<p>To do this, you can use the <code>@for</code> control flow in Angular as seen below:</p>
<pre><code class="lang-typescript">&lt;main&gt;
  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"container"</span>&gt;
    &lt;h3 <span class="hljs-keyword">class</span>=<span class="hljs-string">"mb-3"</span>&gt;Supa Chat &lt;button <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-secondary"</span> style=<span class="hljs-string">"float: right;"</span>&gt;Log
        out&lt;/button&gt;
    &lt;/h3&gt;
    &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"card"</span>&gt;
      &lt;div&gt;

        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"col-12 col-lg-12 col-xl-12"</span>&gt;
          <span class="hljs-meta">@for</span> (msg <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span>.chats(); track msg) {
          &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"position-relative"</span>&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"chat-messages p-4"</span>&gt;
              &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"chat-message-left pb-4"</span>&gt;
                &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"me-5"</span>&gt;
                  &lt;img src={{msg?.users?.avatar_url}} <span class="hljs-keyword">class</span>=<span class="hljs-string">"rounded-circle mr-1"</span> alt=<span class="hljs-string">"image"</span> width=<span class="hljs-string">"40"</span> height=<span class="hljs-string">"40"</span>&gt;
                  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-muted small text-nowrap mt-2"</span>&gt;{{msg?.created_at | date: <span class="hljs-string">'M/d/yy, h:mm a'</span>}}&lt;/div&gt;
                &lt;/div&gt;
                &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex-shrink-1 bg-light rounded py-2 px-3 ml-3"</span>&gt;
                  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"font-weight-bold mb-1"</span>&gt;{{msg?.users?.full_name}}&lt;/div&gt;
                  {{msg?.text}}
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          } <span class="hljs-meta">@empty</span> {
          &lt;div&gt;No chats available&lt;/div&gt;
          }

          &lt;form [formGroup]=<span class="hljs-string">"chatForm"</span> (ngSubmit)=<span class="hljs-string">"onSubmit()"</span>&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex-grow-0 py-3 px-4 border-top"</span>&gt;
              &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"input-group"</span>&gt;
                &lt;input formControlName=<span class="hljs-string">"chat_message"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"form-control"</span> placeholder=<span class="hljs-string">"Type your message"</span>&gt;
                &lt;button [disabled]=<span class="hljs-string">"!chatForm.valid"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;Send&lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/main&gt;
</code></pre>
<p>From the code above, right above the <code>div</code> with the <code>position-relative</code> class, we declared the <code>@for (msg of this.chats(); track msg)</code> control flow, which does the following:</p>
<ul>
<li><p>Loops through the array returned by <code>this.chats()</code> which is the signal variable that was declared in the template.</p>
</li>
<li><p>Assigns each item in the array to the <code>msg</code> variable.</p>
</li>
<li><p>Tracks each item by identity <code>track msg</code> for DOM updates.</p>
</li>
</ul>
<p>Next, within the loop, you called the data in the appropriate HTML tag to display the image, the date the chat was created, the full name, and the chat message as well.</p>
<p>Finally, you created an <code>@empty</code> block which displays the message <code>No chats available</code> if there are no items in the array.</p>
<p>You should have the outcome below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1749082870784/85184bf1-3cfe-4808-a149-64a6b925e097.png" alt="A chat interface displaying two messages from the user &quot;adedeji adesoga,&quot; dated June 4, 2025, with a text input box at the bottom." class="image--center mx-auto" width="1904" height="715" loading="lazy"></p>
<h2 id="heading-how-to-delete-data-in-the-angular-application">How to Delete Data in the Angular Application</h2>
<p>When creating the delete functionality, first you need to create a service function in the <code>chat-service.ts</code> file as seen below:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">async</span> deleteChat(id: <span class="hljs-built_in">string</span>) {
    <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.from(<span class="hljs-string">'chat'</span>).delete().eq(<span class="hljs-string">'id'</span>, id);
    <span class="hljs-keyword">return</span> data;
  }
</code></pre>
<p>All the above function does is find the specific id provided from the parameter, and return the result of the delete operation.</p>
<p>Next, track the selected chat that was clicked from the array of listed chats and then pass the data down to your service.</p>
<p>To do this, first create a function within the service called <code>selectedChats()</code> which helps receive the data from the template:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">public</span> savedChat = signal({});

 selectedChats(msg: Ichat) {
    <span class="hljs-built_in">this</span>.savedChat.set(msg);
  }
</code></pre>
<p>Above, we created the variable called <code>savedChat</code>. It’s declared as a signal that helps receive the object of the chat that we want to delete using the <code>set()</code> method.</p>
<p>You can now head to the <code>chat-component.ts</code> file to create the function that passed the data down to the <code>selectedChats()</code> function.</p>
<p>You can see this function below:</p>
<pre><code class="lang-typescript"> openDropDown(msg: Ichat) {
    <span class="hljs-built_in">console</span>.log(msg);
    <span class="hljs-built_in">this</span>.chat_service.selectedChats(msg);
  }
</code></pre>
<p>As you can see from the function above, once you bind it to the HTML element, it will make sure that you get the object of the specific chat that was clicked.</p>
<p>In our <code>chat-component.html</code> file, create a menu drop down that will help achieve this result as seen below:</p>
<pre><code class="lang-typescript">&lt;main&gt;
  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"container"</span>&gt;
    &lt;h3 <span class="hljs-keyword">class</span>=<span class="hljs-string">"mb-3"</span>&gt;Supa Chat &lt;button <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-secondary"</span> style=<span class="hljs-string">"float: right;"</span>&gt;Log
        out&lt;/button&gt;
    &lt;/h3&gt;
    &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"card"</span>&gt;
      &lt;div&gt;

        &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"col-12 col-lg-12 col-xl-12"</span>&gt;
          <span class="hljs-meta">@for</span> (msg <span class="hljs-keyword">of</span> <span class="hljs-built_in">this</span>.chats(); track msg) {
          &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"position-relative"</span>&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"chat-messages p-4"</span>&gt;
              &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"chat-message-left pb-4"</span>&gt;
                &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"me-5"</span>&gt;
                  &lt;img src={{msg?.users?.avatar_url}} <span class="hljs-keyword">class</span>=<span class="hljs-string">"rounded-circle mr-1"</span> alt=<span class="hljs-string">"image"</span> width=<span class="hljs-string">"40"</span> height=<span class="hljs-string">"40"</span>&gt;
                  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-muted small text-nowrap mt-2"</span>&gt;{{msg?.created_at | date: <span class="hljs-string">'M/d/yy, h:mm a'</span>}}&lt;/div&gt;
                &lt;/div&gt;
                &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex-shrink-1 bg-light rounded py-2 px-3 ml-3"</span>&gt;
                  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"font-weight-bold mb-1"</span>&gt;{{msg?.users?.full_name}}&lt;/div&gt;
                  {{msg?.text}}
                &lt;/div&gt;

                &lt;!-- Delete Modal Button Menu--&gt;
                &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"dropdown"</span>&gt;
                  &lt;span (click)=<span class="hljs-string">"openDropDown(msg)"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"mt-3 ms-5"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span> id=<span class="hljs-string">"dropdownMenuButton1"</span>
                    data-bs-toggle=<span class="hljs-string">"dropdown"</span> aria-expanded=<span class="hljs-string">"false"</span>&gt;
                    ...
                  &lt;/span&gt;
                  &lt;ul <span class="hljs-keyword">class</span>=<span class="hljs-string">"dropdown-menu"</span> aria-labelledby=<span class="hljs-string">"dropdownMenuButton1"</span>&gt;
                    &lt;li&gt;
                      &lt;a <span class="hljs-keyword">class</span>=<span class="hljs-string">"dropdown-item"</span> href=<span class="hljs-string">"#"</span> data-bs-toggle=<span class="hljs-string">"modal"</span> data-bs-target=<span class="hljs-string">"#exampleModal"</span>&gt;Delete&lt;/a&gt;
                    &lt;/li&gt;
                  &lt;/ul&gt;
                &lt;/div&gt;


              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/div&gt;
          } <span class="hljs-meta">@empty</span> {
          &lt;div&gt;No chats available&lt;/div&gt;
          }

          &lt;form [formGroup]=<span class="hljs-string">"chatForm"</span> (ngSubmit)=<span class="hljs-string">"onSubmit()"</span>&gt;
            &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex-grow-0 py-3 px-4 border-top"</span>&gt;
              &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"input-group"</span>&gt;
                &lt;input formControlName=<span class="hljs-string">"chat_message"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"form-control"</span> placeholder=<span class="hljs-string">"Type your message"</span>&gt;
                &lt;button [disabled]=<span class="hljs-string">"!chatForm.valid"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;Send&lt;/button&gt;
              &lt;/div&gt;
            &lt;/div&gt;
          &lt;/form&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/main&gt;
</code></pre>
<p>From the above code, take note of the following: the comment with the text <code>&lt;!-- Delete Modal Button Menu--&gt;</code> is created within the <code>@for</code> control flow. This is essential because it allows the <code>openDropDown(msg)</code> to receive the right object as a parameter when the drop down menu is clicked. A quick look at the console will reveal this.</p>
<p>You can now create the delete modal component, which allows you to consume the delete service required for a chat to be deleted.</p>
<p>To create the delete component, use the command below:</p>
<pre><code class="lang-powershell">ng g component layout/modal<span class="hljs-literal">-component</span>
</code></pre>
<p>The design for the delete component is a Bootstrap 5 modal that looks like this:</p>
<pre><code class="lang-typescript">&lt;!-- Modal --&gt;
&lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal fade"</span> id=<span class="hljs-string">"exampleModal"</span> tabindex=<span class="hljs-string">"-1"</span> aria-labelledby=<span class="hljs-string">"exampleModalLabel"</span> aria-hidden=<span class="hljs-string">"true"</span>&gt;
  &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-dialog"</span>&gt;
    &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-content"</span>&gt;
      &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-header"</span>&gt;
        &lt;h5 <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-title"</span> id=<span class="hljs-string">"exampleModalLabel"</span>&gt;Modal title&lt;/h5&gt;
        &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn-close"</span> data-bs-dismiss=<span class="hljs-string">"modal"</span> aria-label=<span class="hljs-string">"Close"</span>&gt;&lt;/button&gt;
      &lt;/div&gt;
      &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-body"</span>&gt;
        Are really sure you want to <span class="hljs-keyword">delete</span> <span class="hljs-built_in">this</span> message?
      &lt;/div&gt;
      &lt;div <span class="hljs-keyword">class</span>=<span class="hljs-string">"modal-footer"</span>&gt;
        &lt;button <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-secondary"</span> data-bs-dismiss=<span class="hljs-string">"modal"</span>&gt;No&lt;/button&gt;
        &lt;button [attr.data-bs-dismiss]=<span class="hljs-string">"!this.dismiss() === true ? 'modal' : null"</span> (click)=<span class="hljs-string">"deleteChat()"</span> <span class="hljs-keyword">type</span>=<span class="hljs-string">"button"</span>
          <span class="hljs-keyword">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;Yes&lt;/button&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>If you paste the above code directly in your code editor, you’re going to get a host of errors because we have not created the <code>deleteChat()</code> function as well as the <code>dismiss()</code> signal variable in the template file. Let’s go ahead and do that.</p>
<p>The first step in setting up the <code>modal-componet.ts</code> file component is to import the appropriate modules as seen below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Component, effect, inject, signal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/core'</span>;
<span class="hljs-keyword">import</span> { ChatService } <span class="hljs-keyword">from</span> <span class="hljs-string">'../../services/chat.service'</span>;
<span class="hljs-keyword">import</span> { Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'@angular/router'</span>;
</code></pre>
<p>Next, inject the <code>chatservice</code>, <code>Angular router</code>, as well as the the <code>dismiss</code> variable which is a signal as seen below:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">private</span> chat_service = inject(ChatService);
  <span class="hljs-keyword">private</span> router = inject(Router);
  dismiss = signal(<span class="hljs-literal">false</span>);
</code></pre>
<p>With this you can now create the <code>deleteChat()</code> function as seen below:</p>
<pre><code class="lang-typescript">deleteChat() {
    <span class="hljs-keyword">const</span> id = (<span class="hljs-built_in">this</span>.chat_service.savedChat() <span class="hljs-keyword">as</span> { id: <span class="hljs-built_in">string</span> }).id;

    <span class="hljs-built_in">console</span>.log(id);

    <span class="hljs-built_in">this</span>.chat_service
      .deleteChat(id)
      .then(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">let</span> currentUrl = <span class="hljs-built_in">this</span>.router.url;

        <span class="hljs-built_in">this</span>.dismiss.set(<span class="hljs-literal">true</span>);

        <span class="hljs-built_in">this</span>.router
          .navigateByUrl(<span class="hljs-string">'/'</span>, { skipLocationChange: <span class="hljs-literal">true</span> })
          .then(<span class="hljs-function">() =&gt;</span> {
            <span class="hljs-built_in">this</span>.router.navigate([currentUrl]);
          });
      })
      .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(err);
        alert(err.message);
      });
  }
</code></pre>
<ul>
<li><p>The first thing we did under the <code>deleteChat()</code> method was to extract the <code>id</code> from the chat service.</p>
</li>
<li><p>This <code>id</code> is then passed into the <code>deleteChat()</code> method from our service which helps delete the specific chat that was selected.</p>
</li>
<li><p>Once the chat has been deleted, the current route gets reloaded to update the UI.</p>
</li>
</ul>
<p>To activate the modal, you need to import the Modal Component in the <code>chat-component.html</code> file (last line of code below:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"container"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-3"</span>&gt;</span>Supa Chat <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-secondary"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"float: right;"</span>&gt;</span>Log
        out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"card"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>

        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"col-12 col-lg-12 col-xl-12"</span>&gt;</span>
          @for (msg of this.chats(); track msg) {
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"position-relative"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"chat-messages p-4"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"chat-message-left pb-4"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"me-5"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">src</span>=<span class="hljs-string">{{msg?.users?.avatar_url}}</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"rounded-circle mr-1"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">"image"</span> <span class="hljs-attr">width</span>=<span class="hljs-string">"40"</span> <span class="hljs-attr">height</span>=<span class="hljs-string">"40"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"text-muted small text-nowrap mt-2"</span>&gt;</span>{{msg?.created_at | date: 'M/d/yy, h:mm a'}}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-shrink-1 bg-light rounded py-2 px-3 ml-3"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"font-weight-bold mb-1"</span>&gt;</span>{{msg?.users?.full_name}}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
                  {{msg?.text}}
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>

                <span class="hljs-comment">&lt;!-- Delete Modal Button Menu--&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"dropdown"</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">span</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"openDropDown(msg)"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mt-3 ms-5"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"dropdownMenuButton1"</span>
                    <span class="hljs-attr">data-bs-toggle</span>=<span class="hljs-string">"dropdown"</span> <span class="hljs-attr">aria-expanded</span>=<span class="hljs-string">"false"</span>&gt;</span>
                    ...
                  <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
                  <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"dropdown-menu"</span> <span class="hljs-attr">aria-labelledby</span>=<span class="hljs-string">"dropdownMenuButton1"</span>&gt;</span>
                    <span class="hljs-tag">&lt;<span class="hljs-name">li</span>&gt;</span>
                      <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"dropdown-item"</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"#"</span> <span class="hljs-attr">data-bs-toggle</span>=<span class="hljs-string">"modal"</span> <span class="hljs-attr">data-bs-target</span>=<span class="hljs-string">"#exampleModal"</span>&gt;</span>Delete<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
                    <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
                  <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
                <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>


              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          } @empty {
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>No chats available<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          }

          <span class="hljs-tag">&lt;<span class="hljs-name">form</span> [<span class="hljs-attr">formGroup</span>]=<span class="hljs-string">"chatForm"</span> (<span class="hljs-attr">ngSubmit</span>)=<span class="hljs-string">"onSubmit()"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"flex-grow-0 py-3 px-4 border-top"</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"input-group"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">formControlName</span>=<span class="hljs-string">"chat_message"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"form-control"</span> <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"Type your message"</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">button</span> [<span class="hljs-attr">disabled</span>]=<span class="hljs-string">"!chatForm.valid"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-primary"</span>&gt;</span>Send<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span>

<span class="hljs-comment">&lt;!-- modal --&gt;</span>

<span class="hljs-tag">&lt;<span class="hljs-name">app-modal</span> /&gt;</span>
</code></pre>
<p><strong>NOTE</strong>: Don’t forget to import the <strong>ModalComponent</strong> file in the <code>chat-component.ts</code> file to avoid having any errors.</p>
<p>You have now implemented the ability to Insert, read, and delete data. The final implementation is to integrate the logout functionality.</p>
<h2 id="heading-how-to-implement-logout-functionality-in-the-angular-application">How to Implement Logout Functionality in the Angular Application</h2>
<p>Earlier in the tutorial, within the <code>auth-service.ts</code> file, you created a function called <code>signOut()</code> as seen below:</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">async</span> signOut() {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.supabase.auth.signOut();
  }
</code></pre>
<p>In the <code>chat-component.ts</code> file, you will import and inject the <code>sigOut()</code> method. To do this, follow these steps:</p>
<ul>
<li><p>Import and inject the Angular router.</p>
</li>
<li><p>Import and inject the Authentication Service file</p>
</li>
<li><p>Create the <code>logOut()</code> function that consumes the <code>signOut()</code> service:</p>
</li>
</ul>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> logOut() { 
<span class="hljs-built_in">this</span>.auth .signOut() .then(<span class="hljs-function">() =&gt;</span>
 { <span class="hljs-built_in">this</span>.router.navigate([<span class="hljs-string">'/login'</span>]); }) 
.catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
 alert(err.message);
 });
 }
</code></pre>
<ul>
<li>Finally in the <code>chat-component.html</code> file, within the button tag at the top of the page, call the <code>logout()</code> function using the <code>(click)</code> event handler:</li>
</ul>
<pre><code class="lang-xml">  <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"mb-3"</span>&gt;</span>Supa Chat 
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> (<span class="hljs-attr">click</span>)=<span class="hljs-string">"logOut()"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"btn btn-secondary"</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"float: right;"</span>&gt;</span>Log Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
</code></pre>
<p>Once the Log Out button is clicked, the user gets navigated to the Login page and the user state gets reset in the browser.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you learned how to build a real-time chat application using Angular and Supabase. We covered the following key concepts:</p>
<ul>
<li><p>How to create database tables in Supabase</p>
</li>
<li><p>How to create triggers and functions in Supabase</p>
</li>
<li><p>How to use signals to manage state in an Angular</p>
</li>
<li><p>How to create authentication and authorization using Supabase and Google OAuth 2.0</p>
</li>
<li><p>How to work with Reactive forms in Angular</p>
</li>
</ul>
<p>and lots more.</p>
<p>You can access the full codebase by cloning the repository on <a target="_blank" href="https://github.com/desoga10/ng-chat-20">GitHub</a>.</p>
<p>If you found this article helpful, consider subscribing to my <a target="_blank" href="https://www.youtube.com/@TheCodeAngle">YouTube channel</a> where I share hands-on tutorials on modern web development technologies like JavaScript, HTML, CSS, Angular, Supabase, Firebase, React, Third party API and AI tools, and many more. Cheers!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Quickly Add Auth to your Flutter Apps with Supabase Auth UI ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you will learn how to use Supabase's auth package to quickly and efficiently add authentication functionality to your Flutter apps. We will go through the entire process, from setting up a Flutter project to configuring Email/Passwor... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/add-auth-to-flutter-apps-with-supabase-auth-ui/</link>
                <guid isPermaLink="false">66b999a5c39234149cf01130</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Fatuma Abdullahi ]]>
                </dc:creator>
                <pubDate>Mon, 03 Jun 2024 19:55:22 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/05/supa-auth-2.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you will learn how to use Supabase's <a target="_blank" href="https://pub.dev/packages/supabase_auth_ui">auth package</a> to quickly and efficiently add authentication functionality to your Flutter apps. We will go through the entire process, from setting up a Flutter project to configuring Email/Password, OAuth, and Magic link flows.</p>
<p>In the end, you'll have a complete authentication system with theming, localisation, and native support.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li>
        <a href="#prerequisites">Prerequisites</a>
    </li>
    <li>
        <a href="#what-is-supabase-auth-ui">What is Supabase Auth UI</a>
    </li>
    <li>
        <a href="#supported-authentication-methods
        ">Supported Authentication Methods
        </a>
    </li>
    <li>
        <a href="#how-to-set-up-a-flutter-project
        ">How to Set Up a Flutter Project
        </a>
    </li>

    <li>
        <a href="#how-to-connect-to-a-supabase-project
        ">How to Connect to a Supabase Project
        </a>
    </li>
    <li>
        <a href="#how-to-implement-auth-in-a-flutter-app
        ">How to Implement Auth in a Flutter App
        </a>
    </li>

    <li>
        <a href="#how-to-set-up-supabase-email-and-oauth-provider-authentication
                 ">How to Set Up Supabase Email and OAuth Provider Authentication
        </a>
        <ul>
            <li><a href="#how-to-set-up-github-as-an-oauth-provider">How to Set Up GitHub as an OAuth Provider</a>
            </li>
            <li><a href="#how-to-set-up-google-as-an-oauth-provider">How to Set Up Google as an OAuth Provider</a>
            </li>
        </ul>
</li>


<li><a href="#how-to-sign-in-a-user-using-the-auth-ui-package
    ">How to Sign in a User using the Auth UI Package
    </a></li>

<li><a href="#how-to-add-magic-link-auth
    ">How to Add Magic Link Auth
    </a></li>
<li><a href="#native-auth-support-how-to-sign-in-with-google
    ">Native Auth Support - How to Sign in with Google
    </a></li>

   <li>
        <a href="#how-to-theme-your-supabase-auth-ui
                 ">How to Theme Your Supabase Auth UI
        </a>
        <ul>
            <li><a href="#flexible-layout-options">Flexible Layout Options</a>
            </li>
            <li><a href="#localization-and-translation-ready">Localization and Translation Ready</a>
            </li>
        </ul>
</li>



    <li>
        <a href="#summary">
            Summary
        </a>
    </li>
  <li>
        <a href="#resources">
            Resources
        </a>
    </li>

</ul>

<p>## </p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article assumes that you have:</p>
<ul>
<li>A basic understanding of <a target="_blank" href="https://docs.flutter.dev/">Flutter</a></li>
<li>Flutter <a target="_blank" href="https://docs.flutter.dev/get-started/install">installed</a> and ready to go</li>
<li>A basic understanding of <a target="_blank" href="https://www.cloudflare.com/en-gb/learning/serverless/glossary/backend-as-a-service-baas/">Backend-as-a-Service concepts</a></li>
<li>A basic understanding of <a target="_blank" href="https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/">authentication</a></li>
<li>An IDE (Integrated Developer Environment) or a <a target="_blank" href="https://code.visualstudio.com/download">text editor</a> to work in</li>
</ul>
<h2 id="heading-what-is-supabase-auth-ui">What is Supabase Auth UI?</h2>
<p>Supabase Auth UI is an open-source community-supported Flutter package that offers pre-configured, theme-able widgets to simplify the process of creating authentication forms.</p>
<p>What's more? It's translation ready.</p>
<h2 id="heading-supported-authentication-methods">Supported Authentication Methods</h2>
<p>The Supabase auth UI supports the following authentication methods out of the box:</p>
<ul>
<li>Magic links</li>
<li>Email and Password auth</li>
<li>OAuth/Social login auth</li>
<li>Sign in with Google</li>
<li>Sign in with Apple</li>
</ul>
<h2 id="heading-how-to-set-up-a-flutter-project">How to Set Up a Flutter Project</h2>
<p>The first thing you will need is a Flutter project set up. Open your preferred text editor to the location you'd like to keep the Flutter project, then open up the integrated terminal and run <code>flutter create auth_example</code>. This will create a folder called "auth_example" in the same location.</p>
<p>Open the newly created folder and paste the following in the <code>pubspec.yaml</code> file as part of the dependencies: <code>supabase_auth_ui: ^0.4.4</code>. </p>
<p>The file should look similar to this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716931528734/b4e92507-4427-4945-a74e-de8edd0c1107.png" alt="Image" width="1696" height="776" loading="lazy">
_A code snippet showing a Flutter <code>pubspec.yaml</code> file with dependencies. The <code>supabase_auth_ui</code> dependency is highlighted with a pink arrow, indicating version <code>^0.4.4</code>._</p>
<p>Back in the integrated terminal, run <code>cd auth_example</code> to change into the correct folder then run <code>flutter pub get</code> to get the auth dependency into your project.</p>
<h2 id="heading-how-to-connect-to-a-supabase-project">How to Connect to a Supabase Project</h2>
<p>Go to your Supabase dashboard or <a target="_blank" href="https://supabase.com/">create an account</a> if you don't have one. In the dashboard, go to Project settings at the bottom then click on API under configuration. Copy the URL and the project anon key from the right hand side of the page as illustrated below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716932440241/1f5c49a5-5ef8-449f-8bec-557e0492e950.png" alt="Image" width="2822" height="1332" loading="lazy">
<em>A screenshot of the API settings page in Supabase dashboard. The left sidebar shows various menu options under "Project Settings," "Configuration," and "Billing." The "API" option under "Configuration" is highlighted. The main section displays fields for "Project URL" and "Project API keys," with options to copy or reveal the keys.</em></p>
<p>Back in your Flutter app, create a <code>.env</code> file at the root of the folder and paste the following, replacing with the values you copied above:</p>
<pre><code class="lang-bash">SUPABASE_URL=your_url
SUPABASE_ANON_KEY=your_project_anon_key
</code></pre>
<p>Add the environment file to the .<code>gitignore</code> file to keep it out of version control, then add the <code>flutter_dotenv</code> package to the list of dependencies right below <code>supabase_auth_ui</code> dependency. Finally add the <code>.env</code> file as a path under the assets key in the pubspec.yaml file. </p>
<p>The file should look like so:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716933569314/2beb83cc-f8fc-406a-ac39-dedb5d32d42d.png" alt="Image" width="1674" height="1066" loading="lazy">
_A screenshot of a code editor displaying a <code>pubspec.yaml</code> file for a Flutter project. The file lists dependencies such as <code>flutter</code>, <code>supabase_auth_ui</code>, and <code>flutter_dotenv</code>. Two pink arrows point to the <code>supabase_auth_ui: ^0.4.4</code> and <code>flutter_dotenv: ^5.1.0</code> dependencies. The file also includes sections for <code>dev_dependencies</code>, <code>flutter_lints</code>, and <code>assets</code>._</p>
<p>In the <code>main.dart</code> file, replace the <code>main</code> function with the following code:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> dotenv.load(fileName: <span class="hljs-string">".env"</span>);
  <span class="hljs-keyword">await</span> Supabase.initialize(
      url: dotenv.env[<span class="hljs-string">'SUPABASE_URL'</span>]!,
      anonKey: dotenv.env[<span class="hljs-string">'SUPABASE_ANON_KEY'</span>]!);
  runApp(
    <span class="hljs-keyword">const</span> MyApp(),
  );
}
</code></pre>
<p>This loads the <code>.env</code> file and initialises Supabase.</p>
<h2 id="heading-how-to-implement-auth-in-a-flutter-app">How to Implement Auth in a Flutter App</h2>
<p>Replace the rest of the code below the <code>main</code> function with the following:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyApp</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> MyApp({Key? key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> MaterialApp(
      title: <span class="hljs-string">'Supabase Auth UI'</span>,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
      ),
      initialRoute: <span class="hljs-string">'/'</span>,
      routes: {
        <span class="hljs-string">'/'</span>: (context) =&gt; <span class="hljs-keyword">const</span> SplashScreen(),
        <span class="hljs-string">'/auth'</span>: (context) =&gt; AuthScreen(),
        <span class="hljs-string">'/home'</span>: (context) =&gt; HomeScreen(),
      },
    );
  }
}
</code></pre>
<p>Create a <code>splash_screen.dart</code> file in the <code>lib</code> folder and paste the following in it:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:auth_ui_example/auth_screen.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:auth_ui_example/home_screen.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;

<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_auth_ui/supabase_auth_ui.dart'</span>;

<span class="hljs-keyword">final</span> activeSession = Supabase.instance.client.auth.currentSession;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SplashScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> SplashScreen({Key? key}) : <span class="hljs-keyword">super</span>(key: key);

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: Center(child: activeSession == <span class="hljs-keyword">null</span> ? AuthScreen() : HomeScreen()),
    );
  }
}
</code></pre>
<p>This redirects the user to a different screen depending on whether or not the user has an active session.</p>
<p>Now create a new file in the <code>lib</code> folder called <code>home_screen.dart</code> and paste the following into it:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_auth_ui/supabase_auth_ui.dart'</span>;

<span class="hljs-keyword">final</span> supabase = Supabase.instance.client;
<span class="hljs-keyword">final</span> activeSession = supabase.auth.currentSession;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">HomeScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatefulWidget</span> </span>{
  <span class="hljs-keyword">const</span> HomeScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  State&lt;HomeScreen&gt; createState() =&gt; _HomeScreenState();
}

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">_HomeScreenState</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">State</span>&lt;<span class="hljs-title">HomeScreen</span>&gt; </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    <span class="hljs-keyword">if</span> (activeSession == <span class="hljs-keyword">null</span>) {
      Navigator.pushNamed(context, <span class="hljs-string">'/auth'</span>);
    }
  }

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      appBar: AppBar(title: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Home'</span>)),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text(
              <span class="hljs-string">'You are home - <span class="hljs-subst">${activeSession?.user.id}</span>'</span>,
              style: <span class="hljs-keyword">const</span> TextStyle(fontWeight: FontWeight.bold, fontSize: <span class="hljs-number">18</span>),
            ),
            <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">24.0</span>),
            ElevatedButton(
              onPressed: () <span class="hljs-keyword">async</span> {
                <span class="hljs-keyword">await</span> supabase.auth.signOut();
                Navigator.pushNamed(context, <span class="hljs-string">'/'</span>);
              },
              child: <span class="hljs-keyword">const</span> Text(<span class="hljs-string">'Sign out'</span>),
            ),
          ],
        ),
      ),
    );
  }
}
</code></pre>
<p>This creates a stateful widget class that checks for an active session and redirects to the authentications screen if there is no active session. It also displays some text and a button that allows the user to sign out of the application.</p>
<p>Finally, create <code>auth_screen.dart</code> in the <code>lib</code> folder and paste the following into it:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_auth_ui/supabase_auth_ui.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> AuthScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: ListView(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.fromLTRB(<span class="hljs-number">24.0</span>, <span class="hljs-number">96.0</span>, <span class="hljs-number">24.0</span>, <span class="hljs-number">24.0</span>),
        children: [
          Column(
            children: [
              <span class="hljs-keyword">const</span> Text(
                <span class="hljs-string">'Supabase Auth UI'</span>,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: <span class="hljs-number">18</span>,
                ),
              ),
              <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">24.0</span>),
              SupaEmailAuth(
                redirectTo:
                    kIsWeb ? <span class="hljs-keyword">null</span> : <span class="hljs-string">"myapptest://com.example.auth_ui_example"</span>,
                onSignInComplete: (res) =&gt; Navigator.pushNamed(context, <span class="hljs-string">'/home'</span>),
                onSignUpComplete: (res) =&gt; Navigator.pushNamed(context, <span class="hljs-string">'/home'</span>),
                onError: (error) =&gt; SnackBar(content: Text(error.toString())),
              ),
              SupaSocialsAuth(
                socialProviders: <span class="hljs-keyword">const</span> [
                  OAuthProvider.google,
                  OAuthProvider.github,
                ],
                redirectUrl:
                    kIsWeb ? <span class="hljs-keyword">null</span> : <span class="hljs-string">"myapptest://com.example.auth_ui_example"</span>,
                onSuccess: (session) =&gt; Navigator.pushNamed(
                  context,
                  <span class="hljs-string">'/home'</span>,
                ),
                onError: (error) =&gt; SnackBar(
                  content: Text(
                    error.toString(),
                  ),
                ),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>This displays some text and special widgets from the <code>supabase_auth_ui</code> package that display a sign up/in form and some social login options.</p>
<p>In the integrated terminal, run <code>flutter run</code> to start the application. It will give you several platforms to run on, so just choose chrome for this case. You should see this beautiful interface out of the box 🎉:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716935543736/501f4324-0bde-4aac-bc07-a54301d049a7.png" alt="Image" width="2090" height="1116" loading="lazy">
<em>Supabase Auth UI login screen with fields for email and password, options to sign in, reset password, sign up, and buttons to continue with Google or GitHub.</em></p>
<p>Taking a closer look at the code in the auth screen file, you will see that we are using <code>SupaEmailAuth</code> and <code>SupaSocialsAuth</code> widgets to achieve this. The email auth widget takes in a callback telling it what to do on failure and on success of the authentication action. The social login widget takes the same, plus a list of OAuth providers you wish to use in your application.</p>
<h2 id="heading-how-to-set-up-supabase-email-and-oauth-provider-authentication">How to Set Up Supabase Email and OAuth Provider Authentication</h2>
<p>To have Supabase auth UI work properly on both mobile and web, you will need to set up deep links and a site URL.</p>
<p>Under authentication &gt; URL configurations, add "<a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a>" as the site URL.</p>
<p>Then under "redirect URLs" add "YOUR_SCHEME://YOUR_HOSTNAME" as the value, where YOUR_SCHEME is a unique identifier that you decide for your app and YOUR_HOSTNAME is the package name in your <code>build.gradle</code> file as applicationID under android &gt; src &gt; build.gradle. Something like this: "myapptest://com.example.auth_ui_example".</p>
<p>Complete configuring Android by going to the <code>AndroidManifest</code> file under android &gt; src &gt; main &gt; AndroidManifest.xml, and adding this code below the existing <code>&lt;intent-filter&gt;</code>, above the closing <code>&lt;/activity&gt;</code> tag:</p>
<pre><code class="lang-xml"> <span class="hljs-tag">&lt;<span class="hljs-name">intent-filter</span>&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">action</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.action.VIEW"</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.DEFAULT"</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">category</span> <span class="hljs-attr">android:name</span>=<span class="hljs-string">"android.intent.category.BROWSABLE"</span> /&gt;</span>
                <span class="hljs-tag">&lt;<span class="hljs-name">data</span>
                <span class="hljs-attr">android:scheme</span>=<span class="hljs-string">"myapptest"</span>
                <span class="hljs-attr">android:host</span>=<span class="hljs-string">"com.example.auth_ui_example"</span> /&gt;</span>
 <span class="hljs-tag">&lt;/<span class="hljs-name">intent-filter</span>&gt;</span>
</code></pre>
<p>Complete the iOS configuration by pasting the following code at the bottom of the <code>info.plist</code> file, just above the closing <code>&lt;/dict&gt;</code> tag, under ios &gt; Runner &gt; info.plist:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>CFBundleURLTypes<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">dict</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>CFBundleTypeRole<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>Editor<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">key</span>&gt;</span>CFBundleURLSchemes<span class="hljs-tag">&lt;/<span class="hljs-name">key</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">array</span>&gt;</span>
            <span class="hljs-comment">&lt;!-- Add your custom URL scheme here --&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>myapptest<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">dict</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">array</span>&gt;</span>
</code></pre>
<p>That completes the set up across web and mobile.</p>
<p>To set up email auth, in the Supabase dashboard, go to authentication &gt; providers, and look for email. Make sure it is enabled. That's all for setting up email.</p>
<h3 id="heading-how-to-set-up-github-as-an-oauth-provider">How to Set Up GitHub as an OAuth Provider</h3>
<p>To enable GitHub as an authentication provider, scroll down the list of providers until you get to GitHub. Open the dropdown, enable it, and copy the callback URL. Then follow these steps:</p>
<ol>
<li>Create a new OAuth App <a target="_blank" href="https://github.com/settings/developers">here</a>, fill in the required information, and add the copied URL under the authorization URL field.</li>
<li>Add the homepage URL as "<a target="_blank" href="http://localhost:3000/">http://localhost:3000/</a>".</li>
<li>Copy the clientID from GitHub and add it to the Supabase GitHub provider settings.</li>
<li>Generate a new secret and paste it into the client secret field in the Supabase GitHub provider settings.</li>
</ol>
<p>Save it, and you are done with setting up GitHub as a provider.</p>
<h3 id="heading-how-to-set-up-google-as-an-oauth-provider">How to Set Up Google as an OAuth Provider</h3>
<p>Go back to the Supabase dashboard and enable the Google provider. Copy the callback URL, then follow these steps:</p>
<ol>
<li>Create a new <a target="_blank" href="https://www.cloud.google.com/">Google cloud project</a>.</li>
<li>Go to Credentials, click on the create credentials button, and then choose OAuth client ID.</li>
<li>Go on and configure the consent screen to external users.</li>
<li>Go back to credentials, click create credentials, and choose web application in the application type field.</li>
<li>Copy the callback URL from the Supabase settings, paste it in under the authorised URLs field, and leave the rest as is and save.</li>
<li>A screen will pop up with the client ID and secret. Copy those over and paste them in the relevant fields in the Supabase Google provider settings.</li>
</ol>
<p>Save it, and you are done with setting up Google as a provider.</p>
<h2 id="heading-how-to-sign-in-a-user-using-the-auth-ui-package">How to Sign in a User using the Auth UI Package</h2>
<p>To test that auth is working so far, go back to your application, run <code>flutter run -d chrome --web-port=3000</code> in the integrated terminal window, and click one of the Social login buttons.</p>
<p>You should be presented with a window asking you to confirm permissions, after which the app will sign you in. Signing in via email/password should also work.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717072136518/6aed0f14-595a-49f4-9999-f04977d5463f.png" alt="Image" width="2032" height="1198" loading="lazy">
<em>Authorization screen for "Test Flutter auth ui" requesting access to a GitHub account. Options to "Cancel" or "Authorize FatumaA" are available.</em></p>
<p>Now log out of the application and try to manually navigate to the home screen. It will redirect you back to the auth screen.</p>
<h2 id="heading-how-to-add-magic-link-auth">How to Add Magic Link Auth</h2>
<p>If you only want to sign in a user using magic link, replace the contents in the <code>auth_screen</code> file with this:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_auth_ui/supabase_auth_ui.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> AuthScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: ListView(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.fromLTRB(<span class="hljs-number">24.0</span>, <span class="hljs-number">96.0</span>, <span class="hljs-number">24.0</span>, <span class="hljs-number">24.0</span>),
        children: [
          Column(
            children: [
              <span class="hljs-keyword">const</span> Text(
                <span class="hljs-string">'Supabase Auth UI'</span>,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: <span class="hljs-number">18</span>,
                ),
              ),
              <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">24.0</span>),
              SupaMagicAuth(
                redirectUrl:
                    kIsWeb ? <span class="hljs-keyword">null</span> : <span class="hljs-string">"myapptest://com.example.auth_ui_example"</span>,
                onSuccess: (session) =&gt; Navigator.pushNamed(context, <span class="hljs-string">'/home'</span>),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>Run the app and you should see this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/Screenshot-2024-06-01-at-00.16.25.png" alt="Image" width="600" height="400" loading="lazy">
<em>Supabase authentication interface with an email input field and a 'Continue with magic Link' button.</em></p>
<p>Sign in and you should receive an email and be redirected back to the application.</p>
<h2 id="heading-native-auth-support-how-to-sign-in-with-google">Native Auth Support – How to Sign in with Google</h2>
<p>To add native sign in with Google support across Android, Web, and iOS, you'll need to add a few more configurations.</p>
<p>Go back to your Google cloud project and create a new credential. Choose OAuth client ID. Click on Android from the application type dropdown.</p>
<p>Next, add the package name. You can find this in the <code>AndroidManifest.xml</code> file under android &gt; src &gt; main &gt; AndroidManifest.xml or in the <code>build.gradle</code> file as applicationID under android &gt; src &gt; build.gradle.</p>
<p>To generate the SHA1 certificate fingerprint, paste the following code into a terminal window.</p>
<pre><code class="lang-bash">keytool -list -v \
-<span class="hljs-built_in">alias</span> androiddebugkey -keystore ~/.android/debug.keystore
</code></pre>
<p>When prompted for a password, enter "android".  Copy the generated SHA-1 certificate and paste it into the relevant field in your OAuth client ID set up above and create the project. Close the pop up with the Android client ID.</p>
<p>Go back and create a new OAuth client ID, but this time set it as an iOS app. Provide it the same value as the package name from before and create it. Close the pop up as before.</p>
<p>In your Flutter application, paste the following code above the custom URL scheme you previously added in the <code>info.plist</code> file. Replace the part in caps with the first part of the iOS client ID you created above minus the apps.google... part.</p>
<pre><code class="lang-xml">
            <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>com.googleusercontent.apps.FIRST_PART_OF_IOS_CLIENT_ID_MINUS_apps.googleusercontent.com_PART<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
            <span class="hljs-comment">&lt;!-- Leave rest untouched --&gt;</span>           
            <span class="hljs-comment">&lt;!-- Add your custom URL scheme here --&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">string</span>&gt;</span>myapptest<span class="hljs-tag">&lt;/<span class="hljs-name">string</span>&gt;</span>
</code></pre>
<p>In the <code>auth_screen</code> file, replace the <code>SupaSocialsAuth</code> widget with this:</p>
<pre><code class="lang-dart">              SupaSocialsAuth(
                redirectUrl:
                    kIsWeb ? <span class="hljs-keyword">null</span> : <span class="hljs-string">"myapptest://com.example.auth_ui_example"</span>,
                nativeGoogleAuthConfig: NativeGoogleAuthConfig(
                    iosClientId: dotenv.env[<span class="hljs-string">'IOS_CLIENT_ID'</span>]!,
                    webClientId: dotenv.env[<span class="hljs-string">'WEB_CLIENT_ID'</span>]!),
                socialProviders: <span class="hljs-keyword">const</span> [
                  OAuthProvider.google,
                  OAuthProvider.github,
                ],
                onSuccess: (session) =&gt; Navigator.pushNamed(
                  context,
                  <span class="hljs-string">'/home'</span>,
                ),
                onError: (error) =&gt; SnackBar(
                  content: Text(
                    error.toString(),
                  ),
                ),
              ),
</code></pre>
<p>Add the iOS and Web client secret from Google cloud to your <code>.env</code> file and you are ready to test.</p>
<p>Start the Android or iOS emulator. Then back in your Flutter project, run <code>flutter run</code> in your integrated terminal. Something like this should show up:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/05/Screenshot-2024-06-01-at-00.19.35.png" alt="Image" width="600" height="400" loading="lazy">
<em>A mobile app screen displaying the Supabase Auth UI on an iOS emulator. The interface includes fields for entering an email and password, a "Sign In" button, options for password recovery, account sign-up, and login buttons for Google and GitHub.</em></p>
<p>Click on the Google icon and you should be logged in.</p>
<h2 id="heading-how-to-theme-your-supabase-auth-ui">How to Theme Your Supabase Auth UI</h2>
<p>Now that you have confirmed that the package actually works, it's time for the fun part. Supabase auth UI allows you to customise the look and the layout of the widgets.</p>
<p>In <code>main.dart</code>, replace the <code>ThemeData</code> widget under the <code>MaterialApp</code> with the following:</p>
<pre><code class="lang-dart">ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.purple),
        inputDecorationTheme: <span class="hljs-keyword">const</span> InputDecorationTheme(
          border: OutlineInputBorder(),
          focusedBorder: OutlineInputBorder(
            borderSide: BorderSide(color: Colors.purple, width: <span class="hljs-number">2.0</span>),
          ),
        ),
        elevatedButtonTheme: ElevatedButtonThemeData(
          style: ElevatedButton.styleFrom(
            backgroundColor: Colors.deepPurple,
            foregroundColor: Colors.white,
          ),
        ),
      ),
</code></pre>
<p>Notice how the sign up form responds to the theme changes:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1716941053665/46606dcf-386f-4abf-98e1-464bfd4b2955.png" alt="Image" width="2566" height="748" loading="lazy">
<em>A login interface for Supabase Auth UI. It includes fields for entering an email and password, with icons indicating their respective purposes. Below the fields is a purple "Sign In" button. Links for "Forgot your password?" and "Don't have an account? Sign up" are located at the bottom.</em></p>
<h3 id="heading-flexible-layout-options">Flexible Layout Options</h3>
<p>Supabase auth UI allows you to layout the social login buttons vertically and horizontally. To see this in action, add the following line in the <code>SupaSocialsAuth</code> widget: <code>socialButtonVariant: SocialButtonVariant.icon,</code>. The layout should go from this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717081837364/49584d9b-aac2-412c-b197-bbd2edeac8b1.png" alt="Image" width="1015" height="581" loading="lazy">
<em>A login interface for Supabase Auth UI with fields for email and password, a "Sign In" button, options to reset the password or sign up, and buttons to continue with Google or GitHub layout vertically with text and icons.</em></p>
<p>To this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717081856997/ec454669-c11c-4861-83cc-9cd7ca805140.png" alt="Image" width="1966" height="892" loading="lazy">
<em>Login screen for Supabase Auth UI with fields for email and password, a "Sign In" button, options for password recovery, account creation, and login via Google or GitHub in circular icons layout horizontally</em></p>
<h3 id="heading-localization-and-translation-ready">Localization and Translation Ready</h3>
<p>Supabase auth UI is <a target="_blank" href="https://github.com/supabase-community/flutter-auth-ui/pull/76">translation ready</a>, and you can change the labels to any language you wish.</p>
<p>Replace the code in <code>auth_screen</code> with the following:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:supabase_auth_ui/supabase_auth_ui.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AuthScreen</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">StatelessWidget</span> </span>{
  <span class="hljs-keyword">const</span> AuthScreen({<span class="hljs-keyword">super</span>.key});

  <span class="hljs-meta">@override</span>
  Widget build(BuildContext context) {
    <span class="hljs-keyword">return</span> Scaffold(
      body: ListView(
        padding: <span class="hljs-keyword">const</span> EdgeInsets.fromLTRB(<span class="hljs-number">24.0</span>, <span class="hljs-number">96.0</span>, <span class="hljs-number">24.0</span>, <span class="hljs-number">24.0</span>),
        children: [
          Column(
            children: [
              <span class="hljs-keyword">const</span> Text(
                <span class="hljs-string">'Supabase Auth UI'</span>,
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  fontSize: <span class="hljs-number">18</span>,
                ),
              ),
              <span class="hljs-keyword">const</span> SizedBox(height: <span class="hljs-number">24.0</span>),
              SupaEmailAuth(
                redirectTo:
                    kIsWeb ? <span class="hljs-keyword">null</span> : <span class="hljs-string">"myapptest://com.example.auth_ui_example"</span>,
                localization: <span class="hljs-keyword">const</span> SupaEmailAuthLocalization(
                    enterEmail: <span class="hljs-string">"Ingiza barua pepe yako"</span>,
                    validEmailError: <span class="hljs-string">"'الرجاء إدخال عنوان بريد إلكتروني صالح"</span>,
                    enterPassword: <span class="hljs-string">"Ingresa tu contraseña"</span>,
                    passwordLengthError:
                        <span class="hljs-string">'Tafadhali ingiza nenosiri lenye herufi angalau 6'</span>,
                    signIn: <span class="hljs-string">'تسجيل الدخول'</span>,
                    signUp: <span class="hljs-string">'Registrarse'</span>,
                    forgotPassword: <span class="hljs-string">'Umesahau nenosiri lako?'</span>,
                    dontHaveAccount: <span class="hljs-string">'لا تملك حساب؟ سجل'</span>,
                    haveAccount: <span class="hljs-string">'¿Ya tienes una cuenta? Inicia sesión'</span>,
                    sendPasswordReset:
                        <span class="hljs-string">'Tuma barua pepe ya kurekebisha nenosiri'</span>,
                    backToSignIn: <span class="hljs-string">'العودة إلى تسجيل الدخول'</span>,
                    unexpectedError: <span class="hljs-string">'Se produjo un error inesperado'</span>),
                onSignInComplete: (e) =&gt; Navigator.pushNamed(context, <span class="hljs-string">'/home'</span>),
                onSignUpComplete: (e) =&gt; Navigator.pushNamed(context, <span class="hljs-string">'/home'</span>),
                onError: (error) =&gt; SnackBar(content: Text(error.toString())),
              ),
            ],
          ),
        ],
      ),
    );
  }
}
</code></pre>
<p>The screen is now translated to Swahili, Arabic, and Spanish. Run <code>flutter run -d chrome --web-port=3000</code> and you should see this:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717083375350/55e1129c-3216-4997-af9d-bf4c3a1e1828.png" alt="Image" width="2030" height="802" loading="lazy">
<em>A login interface titled "Supabase Auth UI" with fields for entering email and password in different languages. The email field is labeled "Ingiza barua pepe yako" and the password field is labeled "Ingresa tu contraseña." There is a purple login button with Arabic text. Below the button, there are links for forgotten password and account registration in Swahili and Arabic.</em></p>
<p>Type a wrong email and press sign in button to trigger the error messages. Your app should now be a polyglot, as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1717083621863/93ebfce6-c6ff-4d2b-9709-46e429a08f3f.png" alt="Image" width="1986" height="830" loading="lazy">
<em>A login interface titled "Supabase Auth UI" with fields for email and password. The email field contains "ggg" and has an error message in multiple languages. The password field is empty with a placeholder in Spanish. Below, there is a purple login button with text in Arabic. Additional text in Swahili and Arabic is present below the button.</em></p>
<h2 id="heading-summary">Summary</h2>
<p>Supabase auth UI is a package that makes it exceptionally easy to get started with adding authentication flows in your flutter apps. It provides customizable, translation -ready widgets out of the box.</p>
<p>It is open-source and always looking for more contributions. Remember to leave a star on the <a target="_blank" href="https://github.com/supabase-community/flutter-auth-ui">repository</a>.</p>
<h2 id="heading-resources">Resources</h2>
<p>Here are some links that might be useful:</p>
<ul>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/native-mobile-deep-linking?platform=flutter&amp;queryGroups=platform&amp;queryGroups=os&amp;os=android#setting-up-deep-linking">Supabase docs on native deep linking</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/auth-helpers/flutter-auth-ui">Supabase Flutter auth UI docs</a></li>
<li><a target="_blank" href="https://www.youtube.com/watch?v=utMg6fVmX0U">Supabase video on Google Sign In</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Create a Real Time Chat App with Supabase and Angular ]]>
                </title>
                <description>
                    <![CDATA[ We are just published a course on the freeCodeCamp.org YouTube channel that will help you improve your skills with Supabase and Angular. In this course, you will learn how to build a real-time chat application. The course is perfect for anyone lookin... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/create-a-real-time-chat-app-with-supabase-and-angular/</link>
                <guid isPermaLink="false">664cb09360feaea716a2dd37</guid>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Tue, 21 May 2024 14:32:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1716301947626/b68a7c6f-c3cc-40b2-94bd-1f42ea297cce.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>We are just published a course on the freeCodeCamp.org YouTube channel that will help you improve your skills with Supabase and Angular. In this course, you will learn how to build a real-time chat application. The course is perfect for anyone looking to expand their web development skills.</p>
<p>Angular is a popular framework for building web applications. Developed by Google, it allows developers to create dynamic, single-page applications (SPAs) with a clean and efficient architecture. Angular provides a comprehensive solution that includes everything from data binding and dependency injection to routing and state management.</p>
<p>Supabase is an open-source backend-as-a-service platform that helps you build secure and scalable applications. It offers real-time capabilities, a PostgreSQL database, authentication, and storage services. Supabase makes it easy to set up and manage your backend without needing extensive server-side knowledge.</p>
<p>Deji from the Code Angle developed this course. Here are the sections he included in this course.</p>
<p>1. <strong>Introduction and Project Demo</strong></p>
<p>Get an overview of the project and see the final product in action. This will help you understand what you’ll be building throughout the course.</p>
<p>2. <strong>User Interface Setup</strong></p>
<p>Learn how to set up the user interface for your chat application. This section covers the basics of designing a clean and functional UI using Angular.</p>
<p>3. <strong>Setting up a New Supabase Project</strong></p>
<p>Dive into Supabase and set up a new project. Supabase is a powerful backend-as-a-service platform that provides real-time capabilities and a PostgreSQL database.</p>
<p>4. <strong>Setting up Google OAuth using the Google Cloud Platform</strong></p>
<p>Implement Google OAuth to allow users to log in using their Google accounts. This section covers the setup and configuration of Google Cloud Platform to enable authentication.</p>
<p>5. <strong>Angular Router Configuration</strong></p>
<p>Configure the Angular router to manage navigation within your application. This ensures smooth transitions between different parts of your app.</p>
<p>6. <strong>Setting up the Authentication Service</strong></p>
<p>Create an authentication service to handle user login and logout functionality. This service will be integral to managing user sessions.</p>
<p>7. <strong>Creating the CanActivate Route Guard</strong></p>
<p>Implement route guards to protect certain routes within your application. This ensures that only authenticated users can access specific parts of your app.</p>
<p>8. <strong>Setting up the Chat Tables and the Users Table in Supabase</strong></p>
<p>Set up the necessary database tables in Supabase to store chat messages and user information. This section covers the schema design and data management.</p>
<p>9. <strong>Implementing the Functionality to Create a New Chat</strong></p>
<p>Add functionality to create new chat rooms. Users will be able to start new conversations with ease.</p>
<p>10. <strong>Resetting the Form on Submit and Disabling Button for Validation</strong></p>
<p>Ensure your forms are user-friendly by resetting them on submit and disabling buttons for validation. This enhances the overall user experience.</p>
<p>11. <strong>Fetch and Display Chats from Database</strong></p>
<p>Learn how to fetch chat messages from the database and display them in your application. This section covers real-time data updates to keep your chat app dynamic.</p>
<p>12. <strong>Implementing the Delete Message Functionality</strong></p>
<p>Add functionality to delete messages. Users will have the ability to remove their messages from the chat.</p>
<p>13. <strong>Hosting the App using Vercel</strong></p>
<p>Deploy your chat application using Vercel. This section covers the steps to get your app live on the web.</p>
<p>14. <strong>Outro</strong></p>
<p>Wrap up the course with final thoughts and next steps.</p>
<p>You can watch the full course on <a target="_blank" href="https://youtu.be/Dg7bZUFopUo">the freeCodeCamp.org YouTube channel</a> (2-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/Dg7bZUFopUo" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Mobile Quiz App with React Native, ChatGPT and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores.  The application leverages some of Supabase's features, such as authentication and databa... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-mobile-quiz-app/</link>
                <guid isPermaLink="false">66b8fc7a33470f39c663c1a4</guid>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mobile app development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ React Native ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Asaolu ]]>
                </dc:creator>
                <pubDate>Thu, 29 Feb 2024 17:30:26 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/02/Building-a-mobile-quiz-app-with-React-Native--ChatGPT-and-Supabase--1--1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, you'll learn how to build a mobile quiz application that authenticates users, allows them to take tests, and ranks them based on their scores. </p>
<p>The application leverages some of Supabase's features, such as authentication and database storage, to build a secured full-stack mobile application.</p>
<p>Additionally, you'll learn how to create React Native applications with Expo, generate a set of questions and answers from ChatGPT, and perform CRUD operations and user authentication with Supabase.</p>
<p>To fully understand this tutorial, you'll need to have a basic knowledge of React Native and data fetching in React applications.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><a class="post-section-overview" href="#heading-mobile-application-demo">Mobile Application Demo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to set up a React Native application with Expo</a></li>
<li><a class="post-section-overview" href="#heading-how-to-set-up-a-react-native-application-with-expo-1">How to style the React Native application with Tailwind CSS</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-application-screens">How to build the application screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-authentication-screens">How to build the authentication screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-tab-screens">How to build the tab screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-build-the-stack-screens">How to build the stack screens</a></li>
<li><a class="post-section-overview" href="#heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to generate quiz questions and answers from ChatGPT</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-to-react-native">How to add Supabase to React Native</a></li>
<li><a class="post-section-overview" href="#heading-how-to-add-supabase-authentication-to-react-native-applications">How to add Supabase authentication to React Native applications</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-up-new-users">How to sign up new users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-sign-in-existing-users">How to sign in existing users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-log-users-out-of-the-application">How to log users out of the application</a></li>
<li><a class="post-section-overview" href="#heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</a></li>
<li><a class="post-section-overview" href="#heading-how-to-interact-with-the-supabase-database">How to interact with the Supabase database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-save-the-users-score-to-the-database">How to save user's score to the database</a></li>
<li><a class="post-section-overview" href="#heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</a></li>
<li><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></li>
</ul>
<h2 id="heading-mobile-application-demo">Mobile Application Demo</h2>
<p>To preview the application, download <a target="_blank" href="https://expo.dev/client">Expo Go</a> and paste the links below into the app URL field:</p>
<p><strong>Android:</strong> <code>exp://u.expo.dev/update/a4774250-e156-4d34-bcfc-a4f2549c2e1d</code><br><strong>iOS:</strong> <code>exp://u.expo.dev/update/7e5f8ba5-89c4-4c1d-b219-a613ace642df</code></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/app-demo.png" alt="Image" width="600" height="400" loading="lazy">
<em>Scan the QR code to preview the mobile quiz application within the Expo Go application</em></p>
<h2 id="heading-how-to-set-up-a-react-native-application-with-expo">How to Set Up a React Native Application with Expo</h2>
<p>Expo is an open-source platform that allows you to create cross-platform applications easily with JavaScript. It saves us from the complex configurations required to create a native application with the React Native CLI, making it the easiest and fastest way to build and publish React Native apps.</p>
<p>Execute the code snippet below to create a new <a target="_blank" href="https://expo.dev/">Expo</a> project that uses <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> for navigating between screens.</p>
<pre><code class="lang-bash">npx create-expo-app@latest --template tabs@50
</code></pre>
<p><a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a> is an open-source file-based routing system that enables users to navigate between screens easily. It is similar to Next.js, where each file name represents its route name.</p>
<p>Start the development server to ensure that the app is working as expected.</p>
<pre><code class="lang-bash">npx expo start
</code></pre>
<h3 id="heading-how-to-style-the-react-native-application-with-tailwind-css">How to style the React Native application with Tailwind CSS</h3>
<p>Tailwind CSS is a CSS framework that lets you create modern and stunning applications easily. </p>
<p>However, to style Expo applications using Tailwind CSS, you need to install <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">NativeWind</a> – a library that uses Tailwind CSS as its scripting language.</p>
<p>Run the code snippet below to install NativeWind and its dependencies:</p>
<pre><code class="lang-bash">npx expo install nativewind@^4.0.1 react-native-reanimated tailwindcss
</code></pre>
<p>Execute <code>npx tailwindcss init</code> within your terminal to create a <code>tailwind.config.js</code> file. Update the file with the code snippet below:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">/** <span class="hljs-doctag">@type <span class="hljs-type">{import('tailwindcss').Config}</span> </span>*/</span>
<span class="hljs-built_in">module</span>.exports = {
    <span class="hljs-attr">content</span>: [<span class="hljs-string">"./app/**/*.{js,jsx,ts,tsx}"</span>],
    <span class="hljs-attr">presets</span>: [<span class="hljs-built_in">require</span>(<span class="hljs-string">"nativewind/preset"</span>)],
    <span class="hljs-attr">theme</span>: {
        <span class="hljs-attr">extend</span>: {},
    },
    <span class="hljs-attr">plugins</span>: [],
};
</code></pre>
<p>Create a <code>globals.css</code> file within the root of your project and add the Tailwind directives below:</p>
<pre><code class="lang-css"><span class="hljs-keyword">@tailwind</span> base;
<span class="hljs-keyword">@tailwind</span> components;
<span class="hljs-keyword">@tailwind</span> utilities;
</code></pre>
<p>Update the <code>babel.config.js</code> file with the code below:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">module</span>.exports = <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params">api</span>) </span>{
  api.cache(<span class="hljs-literal">true</span>);
  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">presets</span>: [
      [<span class="hljs-string">"babel-preset-expo"</span>, { <span class="hljs-attr">jsxImportSource</span>: <span class="hljs-string">"nativewind"</span> }],
      <span class="hljs-string">"nativewind/babel"</span>,
    ],
  };
};
</code></pre>
<p>Create a <code>metro.config.js</code> file within the root of your project and paste the code snippet below into the file:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { getDefaultConfig } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"expo/metro-config"</span>);
<span class="hljs-keyword">const</span> { withNativeWind } = <span class="hljs-built_in">require</span>(<span class="hljs-string">'nativewind/metro'</span>);

<span class="hljs-keyword">const</span> config = getDefaultConfig(__dirname)

<span class="hljs-built_in">module</span>.exports = withNativeWind(config, { <span class="hljs-attr">input</span>: <span class="hljs-string">'./globals.css'</span> })
</code></pre>
<p>Finally, import the <code>./globals.css</code> file into the <code>app/_layout.tsx</code> file to enable you to style your application with Tailwind CSS:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👉🏻 Within ./app/_layout.tsx</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"../globals.css"</span>;
</code></pre>
<p>Great job on creating the React Native project with Expo! Now, you're ready to add some style using Tailwind CSS. If you encounter any problems while installing NativeWind, check out the <a target="_blank" href="https://www.nativewind.dev/v4/getting-started/expo-router">documentation</a> for a step-by-step guide.</p>
<h2 id="heading-how-to-build-the-application-screens">How to Build the Application Screens</h2>
<p>Here, I'll guide you through building the application screens. They are divided into three categories:</p>
<ul>
<li>The Authentication screens – the register and login screens.</li>
<li>The Tab layout screens – the dashboard, leaderboard, and profile screens.</li>
<li>The Stack screens – the test and test completion screens.</li>
</ul>
<p>The application prompts new users to create an account and log in before allowing access to the Tab layout screens. </p>
<p>On the dashboard screen, users can take tests on various topics. The leaderboard screen showcases the top ten users. Users can log out or preview their previous attempts on the profile page.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/application-demo.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Application Demo</em></p>
<h3 id="heading-how-to-build-the-authentication-screens">How to Build the Authentication Screens</h3>
<p>The authentication screens accept the user's email and password and ensure the credentials are valid before creating an account or granting access to the application.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/auth-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Authentication Screens</em></p>
<p>Create an <code>index.tsx</code> and a <code>register.tsx</code> file within the <code>app</code> folder and a component that accepts the user's email and password using the <a target="_blank" href="https://reactnative.dev/docs/textinput">React Native TextInput</a> component.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, View, TextInput, Pressable, Alert } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { Link, useRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LoginScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [email, setEmail] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [password, setPassword] = useState&lt;<span class="hljs-built_in">string</span>&gt;(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> router = useRouter();

    <span class="hljs-comment">//👇🏻 triggered when the user submits the email &amp; password</span>
    <span class="hljs-keyword">const</span> handleLogin = <span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
            <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
        setLoading(<span class="hljs-literal">true</span>);
        <span class="hljs-built_in">console</span>.log({
            email,
            password,
        });
        router.replace(<span class="hljs-string">"/(tabs)/"</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;View&gt;
        {<span class="hljs-comment">/** -- user interface--*/</span>}
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet stores the user's email and password in states using the React useState hook. The <code>handleLogin</code> function accepts the user's email and password when the form is submitted and ensures that they are not empty before logging them to the console and redirecting the user to the Dashboard page.</p>
<p>You can create the user interface using the code snippet below. It displays the input fields for the user's credentials and an interactive Sign-in button that executes the <code>handleLogin</code> function. Additionally, the <code>loading</code> state ensures that the button is only pressed once.</p>
<pre><code class="lang-typescript">&lt;View className=<span class="hljs-string">' flex-1'</span>&gt;
    &lt;View className=<span class="hljs-string">'w-full px-4'</span>&gt;
        &lt;Text className=<span class="hljs-string">'text-3xl mb-4 font-bold text-white text-center'</span>&gt;
            Log <span class="hljs-keyword">in</span>
        &lt;/Text&gt;

        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Email Address&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            value={email}
            onChangeText={setEmail}
        /&gt;
        &lt;Text className=<span class="hljs-string">'text-lg text-gray-200'</span>&gt;Password&lt;/Text&gt;
        &lt;TextInput
            className=<span class="hljs-string">'w-full border-b-[1px] py-4 rounded-md mb-3 text-white font-bold'</span>
            secureTextEntry
            value={password}
            onChangeText={setPassword}
        /&gt;
        &lt;Pressable
            className={<span class="hljs-string">`w-full <span class="hljs-subst">${
                loading ? <span class="hljs-string">"bg-orange-200"</span> : <span class="hljs-string">"bg-orange-600"</span>
            }</span> rounded-xl p-4 border-[1px] border-orange-200`</span>}
            disabled={loading}
            onPress={<span class="hljs-function">() =&gt;</span> handleLogin()}
        &gt;
            &lt;Text className=<span class="hljs-string">'text-white text-center font-bold text-xl'</span>&gt;
                {loading ? <span class="hljs-string">"Authenticating..."</span> : <span class="hljs-string">"Sign in"</span>}
            &lt;/Text&gt;
        &lt;/Pressable&gt;
        &lt;Text className=<span class="hljs-string">'text-center mt-2 text-orange-200'</span>&gt;
            Don<span class="hljs-string">'t have an account?{" "}
            &lt;Link href='</span>/register<span class="hljs-string">'&gt;
                &lt;Text className='</span>text-white<span class="hljs-string">'&gt;Register&lt;/Text&gt;
            &lt;/Link&gt;
        &lt;/Text&gt;
    &lt;/View&gt;
&lt;/View&gt;</span>
</code></pre>
<p>For instance, the <code>loading</code> state becomes true when a user clicks the Sign-in button. The Pressable component (button) has a <code>disabled</code> attribute set to the <code>loading</code> state to ensure that the user does not press the button multiple times. Additionally, you can use the loading state to notify the user that the request is processing.</p>
<p>The <code>register.tsx</code> file is also similar to the <code>login.tsx</code> file. You only need to change the words from Login to Register.</p>
<h3 id="heading-how-to-build-the-tab-screens">How to Build the Tab Screens</h3>
<p>The Tab Screens consist of the Dashboard, Leaderboard, and Profile screens.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/tab-screens.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Tab Screens</em></p>
<p>Create a <code>(tabs)</code> folder containing <code>index.tsx</code>, <code>leaderboard.tsx</code>, <code>profile.tsx</code>, and <code>_layout.tsx</code> files within the <code>app</code> folder.</p>
<pre><code class="lang-bash"><span class="hljs-built_in">cd</span> app
mkdir (tabs)
<span class="hljs-built_in">cd</span> (tabs)
touch index.tsx leaderboard.tsx profile.tsx _layout.tsx
</code></pre>
<p>After creating the <code>_layout.tsx</code> file within the (tabs) folder, update the <code>_layout.tsx</code> to specify Tab screen navigation for the newly created screens. The screens use icons from the <a target="_blank" href="https://icons.expo.fyi/Index">Expo Vector Icons library</a>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { Ionicons, MaterialIcons, FontAwesome5 } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Tabs
            screenOptions={{
                tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                tabBarShowLabel: <span class="hljs-literal">false</span>,
                headerShown: <span class="hljs-literal">false</span>,
                tabBarStyle: {
                    backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                    borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                },
            }}
        &gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'index'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;Ionicons name=<span class="hljs-string">'home'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'leaderboard'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;MaterialIcons name=<span class="hljs-string">'leaderboard'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
            &lt;Tabs.Screen
                name=<span class="hljs-string">'profile'</span>
                options={{
                    tabBarIcon: <span class="hljs-function">(<span class="hljs-params">{ color }</span>) =&gt;</span> (
                        &lt;FontAwesome5 name=<span class="hljs-string">'user-alt'</span> size={<span class="hljs-number">24</span>} color={color} /&gt;
                    ),
                }}
            /&gt;
        &lt;/Tabs&gt;
    );
}
</code></pre>
<p>Next, update the <code>RootLayoutNav</code> component within the <code>_app/layout.tsx</code> file to render all the screens within the application.</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
            &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
        &lt;/Stack&gt;
    );
}
</code></pre>
<h4 id="heading-the-dashboard-screen">The Dashboard Screen</h4>
<p>Update the component to allow users to select four categories from a list of categories.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">HomeScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> greet = getGreeting();
    <span class="hljs-keyword">const</span> router = useRouter();
    <span class="hljs-keyword">const</span> { session } = useAuth();
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [userCategories, setUserCategories] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> fetchQuestions = <span class="hljs-keyword">async</span> () =&gt; {};

    <span class="hljs-keyword">const</span> handleStartTest = <span class="hljs-keyword">async</span> () =&gt; {
        Alert.alert(<span class="hljs-string">"Start Test"</span>, <span class="hljs-string">"Are you sure you want to start the test?"</span>, [
            {
                text: <span class="hljs-string">"Cancel"</span>,
                style: <span class="hljs-string">"destructive"</span>,
            },
            {
                text: <span class="hljs-string">"Yes"</span>,
                onPress: <span class="hljs-function">() =&gt;</span> fetchQuestions(),
            },
        ]);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 px-4 py-2'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex flex-row items-center justify-between mb-2'</span>&gt;
                &lt;View&gt;
                    &lt;Text className=<span class="hljs-string">'font-bold text-2xl mb-[1px]'</span>&gt;
                        Good morning
                        &lt;Ionicons name=<span class="hljs-string">'partly-sunny-sharp'</span> size={<span class="hljs-number">24</span>} color=<span class="hljs-string">'orange'</span> /&gt;
                    &lt;/Text&gt;

                    &lt;Text className=<span class="hljs-string">'text-lg'</span>&gt;Welcome User&lt;/Text&gt;
                &lt;/View&gt;
            &lt;/View&gt;
            {userCategories.length === <span class="hljs-number">4</span> &amp;&amp; (
                &lt;Pressable
                    className={<span class="hljs-string">`w-full h-[70px] flex items-center justify-center <span class="hljs-subst">${
                        loading ? <span class="hljs-string">"bg-orange-300"</span> : <span class="hljs-string">"bg-orange-500"</span>
                    }</span> rounded-xl mb-2`</span>}
                    disabled={loading}
                    onPress={<span class="hljs-function">() =&gt;</span> handleStartTest()}
                &gt;
                    &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-50'</span>&gt;
                        {loading ? <span class="hljs-string">"Loading questions..."</span> : <span class="hljs-string">"START TEST"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            )}

            &lt;View className=<span class="hljs-string">'w-full flex-1'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-xl font-bold text-orange-500 mb-4'</span>&gt;
                    Available Categories
                &lt;/Text&gt;
                &lt;FlatList
                    data={categories}
                    numColumns={<span class="hljs-number">2</span>}
                    contentContainerStyle={{ width: <span class="hljs-string">"100%"</span>, gap: <span class="hljs-number">10</span> }}
                    columnWrapperStyle={{ gap: <span class="hljs-number">10</span> }}
                    renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> (
                        &lt;Categories
                            item={item}
                            userCategories={userCategories}
                            setUserCategories={setUserCategories}
                        /&gt;
                    )}
                    showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
                    keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.id}
                /&gt;
            &lt;/View&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a list of categories where users can select only four categories to answer questions on and start the quiz.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/dashboard-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Dashboard Screen</em></p>
<h4 id="heading-the-leaderboard-screen">The Leaderboard Screen</h4>
<p>The Leaderboard screen displays the top ten users ranked in descending order.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Text, FlatList, SafeAreaView } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> Board <span class="hljs-keyword">from</span> <span class="hljs-string">"../../components/Board"</span>;

<span class="hljs-keyword">interface</span> Props {
    total_score: <span class="hljs-built_in">number</span>;
    user_id: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">LeaderboardScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [leaderboard, setLeaderboard] = useState&lt;Props[]&gt;([]);

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;Text className=<span class="hljs-string">'text-2xl font-bold text-gray-500 text-center mb-6'</span>&gt;
                Leaderboard
            &lt;/Text&gt;

            &lt;FlatList
                data={leaderboard}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Board item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.user_id}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above renders a FlatList with ten items. You can create an array containing ten users and pass it into the FlatList for now.</p>
<h4 id="heading-the-profile-screen">The Profile Screen</h4>
<p>The Profile Screen displays the user's image, recent attempts, and a log-out button that enables the user to sign out of the application.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ProfileScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState&lt;<span class="hljs-built_in">boolean</span>&gt;(<span class="hljs-literal">false</span>);
    <span class="hljs-keyword">const</span> [total_score, setTotalScore] = useState&lt;<span class="hljs-built_in">number</span>&gt;(<span class="hljs-number">0</span>);
    <span class="hljs-keyword">const</span> [attempts, setAttempts] = useState&lt;<span class="hljs-built_in">string</span>[]&gt;([]);

    <span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
        setLoading(<span class="hljs-literal">true</span>);
    };

    <span class="hljs-keyword">return</span> (
        &lt;SafeAreaView className=<span class="hljs-string">'flex-1 bg-orange-100 p-4'</span>&gt;
            &lt;View className=<span class="hljs-string">'flex items-center justify-center mb-6'</span>&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-[1px]'</span>&gt;
                    &lt;FontAwesome name=<span class="hljs-string">'star'</span> size={<span class="hljs-number">20</span>} color=<span class="hljs-string">'red'</span> /&gt;
                    &lt;Text&gt;<span class="hljs-number">45</span>&lt;/Text&gt;
                &lt;/Text&gt;
                &lt;Text className=<span class="hljs-string">'text-gray-600 mb-2'</span>&gt;<span class="hljs-meta">@dhastix</span>&lt;/Text&gt;

                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> handleSignOut()} disabled={loading}&gt;
                    &lt;Text className=<span class="hljs-string">'text-red-500'</span>&gt;
                        {loading ? <span class="hljs-string">"Logging out..."</span> : <span class="hljs-string">"Log out"</span>}
                    &lt;/Text&gt;
                &lt;/Pressable&gt;
            &lt;/View&gt;

            &lt;Text className=<span class="hljs-string">'font-bold text-xl text-gray-700 mb-3 px-4'</span>&gt;
                Recent Attempts
            &lt;/Text&gt;

            &lt;FlatList
                data={attempts}
                contentContainerStyle={{ padding: <span class="hljs-number">15</span> }}
                renderItem={<span class="hljs-function">(<span class="hljs-params">{ item }</span>) =&gt;</span> &lt;Attempts item={item} /&gt;}
                keyExtractor={<span class="hljs-function">(<span class="hljs-params">item, index</span>) =&gt;</span> index.toString()}
                showsVerticalScrollIndicator={<span class="hljs-literal">false</span>}
            /&gt;
        &lt;/SafeAreaView&gt;
    );
}
</code></pre>
<p>The code snippet above displays the user's image, the sign-out button, and all the user's attempts. You can create an array of items for testing purposes.</p>
<h3 id="heading-how-to-build-the-stack-screens">How to Build the Stack Screens</h3>
<p>The Stack Screens comprise two screens – the quiz screen and the screen that displays the user's score after completing a quiz session.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screens-1.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Stack Screens</em></p>
<h4 id="heading-the-quiz-screen">The Quiz Screen</h4>
<p>The Quiz Screen displays a timer that countdowns from 15 seconds before moving to the next question. It shows the question, its category, available options, the Skip and Next buttons, and a cancel icon.</p>
<p>Create a similar screen to the one shown below. You can use <a target="_blank" href="https://github.com/dha-stix/techtest-app/blob/main/app/(stack)/test.tsx">this</a> as a guide.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/test-screen.gif" alt="Image" width="600" height="400" loading="lazy">
<em>The Quiz Screen</em></p>
<h4 id="heading-the-quiz-completion-screen">The Quiz Completion Screen</h4>
<p>It displays the user's score after completing a test.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> {
    SafeAreaView,
    Text,
    Pressable,
    View,
    ImageBackground,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;
<span class="hljs-keyword">import</span> { MaterialIcons } <span class="hljs-keyword">from</span> <span class="hljs-string">"@expo/vector-icons"</span>;
<span class="hljs-keyword">import</span> { useLocalSearchParams } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">CompletedScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { score } = useLocalSearchParams();

    <span class="hljs-keyword">return</span> (
        &lt;View className=<span class="hljs-string">'flex flex-1 bg-orange-400'</span>&gt;
            &lt;ImageBackground
                source={{ uri: <span class="hljs-string">"https://source.unsplash.com/NAP14GEjvh8"</span> }}
                className=<span class="hljs-string">'flex-1 p-4'</span>
            &gt;
                &lt;SafeAreaView /&gt;
                &lt;Pressable onPress={<span class="hljs-function">() =&gt;</span> router.replace(<span class="hljs-string">"/(tabs)/"</span>)}&gt;
                    &lt;MaterialIcons name=<span class="hljs-string">'cancel'</span> size={<span class="hljs-number">60</span>} color=<span class="hljs-string">'white'</span> /&gt;
                &lt;/Pressable&gt;

                &lt;View className=<span class="hljs-string">'flex-1 flex items-center justify-center'</span>&gt;
                    &lt;View className=<span class="hljs-string">'bg-orange-50 w-full py-[50px] rounded-xl p-4 flex items-center justify-center shadow-lg shadow-orange-500'</span>&gt;
                        &lt;Text className=<span class="hljs-string">'text-3xl text-orange-600 font-bold mb-4'</span>&gt;
                            {<span class="hljs-built_in">Number</span>(score) &gt; <span class="hljs-number">20</span> ? <span class="hljs-string">"Congratulations🥳"</span> : <span class="hljs-string">"Sorry! You lose 🥲"</span>}
                        &lt;/Text&gt;
                        &lt;Text className=<span class="hljs-string">'font-bold text-xl'</span>&gt;You scored {score}!&lt;/Text&gt;
                    &lt;/View&gt;
                &lt;/View&gt;
            &lt;/ImageBackground&gt;
        &lt;/View&gt;
    );
}
</code></pre>
<p>The code snippet above accepts the user's score as a parameter after completing the quiz and displays the score to the user.</p>
<h2 id="heading-how-to-generate-quiz-questions-and-answers-from-chatgpt">How to Generate Quiz Questions and Answers from ChatGPT</h2>
<p>When building a quiz application, the first question is: how do you get the questions and options for the application? You can either create a list of questions or search for a suitable public API.</p>
<p>However, I'll guide you through creating a list of questions and options in JSON format using ChatGPT. Use this prompt to generate questions and answers from ChatGPT:</p>
<blockquote>
<p><em>Generate 25 distinct questions on  and ensure they are in JSON format containing an id, category which is , a question attribute containing the question, an options array of 3 options, and an answer property.</em></p>
</blockquote>
<p>The prompt returns a JSON result containing the questions and answers. You can host them on GitHub or save them to a database.</p>
<p>The questions and answers I'm using in this mobile application are available on <a target="_blank" href="https://github.com/dha-stix/trivia-app/tree/main/questions">GitHub</a>. Feel free to clone or copy the files.</p>
<p>Once your questions and answers are ready, you can connect the application to Supabase.</p>
<h2 id="heading-how-to-add-supabase-to-react-native">How to Add Supabase to React Native</h2>
<p>Supabase is an open-source Firebase alternative that enables you to create secured and scalable software applications within a few minutes.</p>
<p>It provides a secured Postgres database, a complete user management system that handles various forms of authentication (including email and password, email sign-in, and social authentication), a file storage system that lets you store and serve files of any size, real-time communication, and many other features.</p>
<p>In this tutorial, I'll walk you through the following:</p>
<ul>
<li>How to authenticate users and control access to some application screens with Supabase.</li>
<li>How to save the users' scores to the database to enable you to rank them based on their scores.</li>
</ul>
<p>First, you need to install Supabase and its required dependencies. You can do that with the following commands:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js 
npm install react-native-elements @react-native-async-storage/async-storage react-native-url-polyfill
npx expo install expo-secure-store
</code></pre>
<p>Create a <code>supabase.ts</code> file within your project and copy the code snippet below into the file to initiate Supabase:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-string">"react-native-url-polyfill/auto"</span>;
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> SecureStore <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-secure-store"</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;

<span class="hljs-keyword">const</span> ExpoSecureStoreAdapter = {
    getItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        <span class="hljs-keyword">return</span> SecureStore.getItemAsync(key);
    },
    setItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, value: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.setItemAsync(key, value);
    },
    removeItem: <span class="hljs-function">(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
        SecureStore.deleteItemAsync(key);
    },
};

<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_URL"</span>;
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-string">"YOUR_REACT_NATIVE_SUPABASE_ANON_KEY"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> supabase = createClient(supabaseUrl, supabaseAnonKey, {
    auth: {
        storage: ExpoSecureStoreAdapter <span class="hljs-keyword">as</span> <span class="hljs-built_in">any</span>,
        autoRefreshToken: <span class="hljs-literal">true</span>,
        persistSession: <span class="hljs-literal">true</span>,
        detectSessionInUrl: <span class="hljs-literal">false</span>,
    },
});
</code></pre>
<p>Next, visit the <a target="_blank" href="https://supabase.com">Supabase homepage</a>, sign in, and create a new organization and project.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/project.png" alt="Image" width="600" height="400" loading="lazy">
<em>Create a new Supabase project</em></p>
<p>Click the Settings icon on the sidebar and select API to copy the project URL and the public API key.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-17.50.34.png" alt="Image" width="600" height="400" loading="lazy">
<em>Supabase API settings containing project credentials</em></p>
<p>Create a <code>.env.local</code> file and copy the credentials into the variables. Update the <code>supabase.ts</code> file to use the Supabase URL and API key.</p>
<pre><code class="lang-txt">EXPO_PUBLIC_API_URL=&lt;YOUR_SUPABASE_URL&gt;
EXPO_PUBLIC_API_KEY=&lt;YOUR_SUPABASE_API_KEY&gt;
</code></pre>
<p>Congratulations! You can now interact with Supabase from your application and access various features such as authentication, database, file storage, and so on.</p>
<h2 id="heading-how-to-add-supabase-authentication-to-react-native-applications">How to Add Supabase Authentication to React Native Applications</h2>
<p>Supabase offers various forms of authentication. But we only need the email and password method of authentication for this application.</p>
<h3 id="heading-how-to-sign-up-new-users">How to sign up new users</h3>
<p>The code snippet below accepts an email and a password and creates an account for the user. Otherwise, it returns an error if any of the credentials is invalid.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign up function</span>
<span class="hljs-keyword">const</span> handleRegister = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/"</span>);
};
</code></pre>
<p>With the <code>supabase.auth.signUp()</code> function, Supabase handles the authentication process. If successful, the user is redirected to the login page. Otherwise, it displays an error message.</p>
<h3 id="heading-how-to-sign-in-existing-users">How to sign in existing users</h3>
<p>This function allows existing users to access the application. It accepts the user's email and password and logs the user into the application.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 register function</span>
<span class="hljs-keyword">const</span> handleLogin = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!email.trim() || !password.trim())
        <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, <span class="hljs-string">"Please fill in all fields"</span>);
    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signInWithPassword({ email, password });
    <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> Alert.alert(<span class="hljs-string">"Error"</span>, error.message);
    router.replace(<span class="hljs-string">"/(tabs)/"</span>);
};
</code></pre>
<p>The <code>supabase.auth.signInWithPassword()</code> function validates the user's email and password and redirects the user to the Dashboard screen. Otherwise, it returns the necessary authentication error.</p>
<h3 id="heading-how-to-log-users-out-of-the-application">How to log users out of the application</h3>
<p>Supabase also allows users to sign out of the application. You can execute this function when the user clicks a button within the Profile page.</p>
<pre><code class="lang-typescript"><span class="hljs-comment">//👇🏻 import supabase from supabase file</span>
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/supabase"</span>;

<span class="hljs-comment">//👇🏻 sign out function</span>
<span class="hljs-keyword">const</span> handleSignOut = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
    } <span class="hljs-keyword">catch</span> (error) {
        <span class="hljs-built_in">console</span>.log(error);
    }
};
</code></pre>
<h3 id="heading-how-to-protect-screens-from-unauthenticated-users">How to protect screens from unauthenticated users</h3>
<p>You've been able to add the sign-up, sign-in, and log-out functionalities to the React Native application. But the Dashboard and other screens containing sensitive data are still accessible to unauthenticated users.</p>
<p>How do we fix this?</p>
<p>In this section, I'll walk you through how to protect screens from unauthorized users using the <a target="_blank" href="https://react.dev/reference/react/createContext">React Context API</a>.</p>
<p>The React Context API allows us to pass data through the component tree without needing to pass props down manually at every level.</p>
<p>Create an <code>AuthProvider.tsx</code> file. This is where the data to be passed down the application screens is stored. Copy the code snippet below into the file:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;
<span class="hljs-keyword">import</span> { Session } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/supabase-js"</span>;
<span class="hljs-keyword">import</span> {
    PropsWithChildren,
    createContext,
    useContext,
    useEffect,
    useState,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;

<span class="hljs-keyword">type</span> AuthData = {
    session: Session | <span class="hljs-literal">null</span>;
    loading: <span class="hljs-built_in">boolean</span>;
};

<span class="hljs-comment">//👇🏻 data to be passed down the components</span>
<span class="hljs-keyword">const</span> AuthContext = createContext&lt;AuthData&gt;({
    session: <span class="hljs-literal">null</span>,
    loading: <span class="hljs-literal">true</span>,
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AuthProvider</span>(<span class="hljs-params">{ children }: PropsWithChildren</span>) </span>{
    <span class="hljs-keyword">const</span> [session, setSession] = useState&lt;Session | <span class="hljs-literal">null</span>&gt;(<span class="hljs-literal">null</span>);
    <span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);

    <span class="hljs-comment">//👇🏻 fetches the current user's session</span>
    useEffect(<span class="hljs-function">() =&gt;</span> {
        <span class="hljs-keyword">const</span> fetchSession = <span class="hljs-keyword">async</span> () =&gt; {
            <span class="hljs-keyword">const</span> {
                data: { session },
            } = <span class="hljs-keyword">await</span> supabase.auth.getSession();
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        };

        fetchSession();
        supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">_event, session</span>) =&gt;</span> {
            setSession(session);
            setLoading(<span class="hljs-literal">false</span>);
        });
    }, []);

    <span class="hljs-keyword">return</span> (
        &lt;AuthContext.Provider value={{ session, loading }}&gt;
            {children}
        &lt;/AuthContext.Provider&gt;
    );
}
<span class="hljs-comment">//👇🏻 custom hook for using the context (data)</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> useAuth = <span class="hljs-function">() =&gt;</span> useContext(AuthContext);
</code></pre>
<p>The code snippet retrieves the current user's session. If the user is signed in, the session and loading state variables are updated to show that the user is active, and they are passed into other components within the application.</p>
<p>The <code>useAuth</code> custom hook allows you to access the state variables (session and loading) within the application screens.</p>
<p>To access the context (data) available within the application screens, wrap the entire application with the <code>AuthProvider</code>. So now, update the <code>RootLayoutNav</code> component within the <code>app/_layout.tsx</code> file as shown below:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> AuthProvider <span class="hljs-keyword">from</span> <span class="hljs-string">"../lib/AuthProvider"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayoutNav</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">return</span> (
        &lt;AuthProvider&gt;
            &lt;Stack screenOptions={{ headerShown: <span class="hljs-literal">false</span> }}&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(tabs)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'(stack)'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'index'</span> /&gt;
                &lt;Stack.Screen name=<span class="hljs-string">'register'</span> /&gt;
            &lt;/Stack&gt;
        &lt;/AuthProvider&gt;
    );
}
</code></pre>
<p>Congratulations! You've successfully set up the context. Next, how do we read the context and ensure that only authenticated users can view some of the application screens?</p>
<p>You can do this using the custom <code>useAuth</code> hook. For example, you can protect the Tabs screens via the <code>(tabs)/_layout.tsx</code> file.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Tabs, Redirect } <span class="hljs-keyword">from</span> <span class="hljs-string">"expo-router"</span>;
<span class="hljs-keyword">import</span> { useAuth } <span class="hljs-keyword">from</span> <span class="hljs-string">"../../lib/AuthProvider"</span>;
<span class="hljs-keyword">import</span> { ActivityIndicator } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-native"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TabScreen</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { session, loading } = useAuth();

    <span class="hljs-keyword">if</span> (!session) {
        <span class="hljs-keyword">return</span> &lt;Redirect href=<span class="hljs-string">'/'</span> /&gt;;
    }

    <span class="hljs-keyword">if</span> (loading) {
        <span class="hljs-keyword">return</span> &lt;ActivityIndicator size=<span class="hljs-string">'large'</span> color=<span class="hljs-string">'#f97316'</span> /&gt;;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">return</span> (
            &lt;Tabs
                screenOptions={{
                    tabBarActiveTintColor: <span class="hljs-string">"#f97316"</span>,
                    tabBarInactiveTintColor: <span class="hljs-string">"gray"</span>,
                    tabBarShowLabel: <span class="hljs-literal">false</span>,
                    headerShown: <span class="hljs-literal">false</span>,
                    tabBarStyle: {
                        backgroundColor: <span class="hljs-string">"#ffedd5"</span>,
                        borderTopColor: <span class="hljs-string">"#ffedd5"</span>,
                    },
                }}
            &gt;
                {<span class="hljs-comment">/**-- screens--*/</span>}
            &lt;/Tabs&gt;
        );
    }
}
</code></pre>
<p>The code snippet above checks if there is a session for the current user. If null, the application redirects the user to the login screen. If the application is yet to determine the user's status, it displays a loading icon.</p>
<h2 id="heading-how-to-interact-with-the-supabase-database">How to Interact with the Supabase Database</h2>
<p>In this section, I'll walk you through creating the database for the mobile application. You'll learn how to store and retrieve the user's scores and rank them based on their total score.</p>
<p>Before we proceed, note that the application calculates the user's score after answering each question on the test screen. Upon completion, the user's score is retrieved and displayed on the test completion screen.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> handleSave = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-comment">//👇🏻 checks if the user has not completed the test</span>
    <span class="hljs-keyword">if</span> (count &lt; questions.length - <span class="hljs-number">1</span>) {
        <span class="hljs-comment">//👇🏻 updates the user's score if the selected answer is correct</span>
        <span class="hljs-keyword">if</span> (questions[count].answer === userAnswer) {
            setUserScore(<span class="hljs-function">(<span class="hljs-params">userScore</span>) =&gt;</span> userScore + <span class="hljs-number">1</span>);
        }
        <span class="hljs-comment">//👇🏻 change the question, refresh the selected answer and time</span>
        setCount(<span class="hljs-function">(<span class="hljs-params">count</span>) =&gt;</span> count + <span class="hljs-number">1</span>);
        setSelectedBox(<span class="hljs-literal">null</span>);
        setTime(<span class="hljs-number">15</span>);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">//👇🏻 test completed</span>
        router.push({
            pathname: <span class="hljs-string">"/(stack)/completed"</span>,
            params: { score: userScore },
        });
    }
};
</code></pre>
<p>Within your Supabase project, select Table Editor from the sidebar menu and create a new table containing the following columns:</p>
<ul>
<li><code>id</code> – contains a unique ID for each row of data.</li>
<li><code>created_at</code> – represents the time the data was created.</li>
<li><code>attempts</code> – a text array containing the score and date attributes.</li>
<li><code>total_score</code> – represents a user's cumulative score. We'll rank users using this score</li>
<li><code>user_id</code> – a unique ID used to identify each user's data.</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.05.55.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Columns</em></p>
<p>Finally, you can add a Row Level Security that allows only authenticated users interact with the database.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/02/Screenshot-2024-02-24-at-10.20.51.png" alt="Image" width="600" height="400" loading="lazy">
<em>The Table Row Level Security Policy</em></p>
<h3 id="heading-how-to-save-the-users-score-to-the-database">How to save the user's score to the database</h3>
<p>Before you can save a user's score to the database, you need to check if the user's data already exists – meaning the user has taken a test before. If true, you need to update the user's score with the latest test score. Otherwise, add the data to the database.</p>
<p>The code snippet below accepts the user's score and user's ID (from the session data) and saves the user's score to Supabase.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> saveScore = <span class="hljs-keyword">async</span> (userScore: <span class="hljs-built_in">number</span>, userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">//👇🏻 check if the user data exists</span>
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select()
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;

        <span class="hljs-comment">//👇🏻 if the user data does not exist, insert a new one</span>
        <span class="hljs-keyword">if</span> (error || !data.length) {
            <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .insert({
                    attempts: [{ score: userScore, date: getCurrentDate() }],
                    total_score: userScore,
                    user_id: userID,
                })
                .single();
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-comment">//👇🏻 if the user data exists, update the attempts and total_score</span>
            <span class="hljs-keyword">const</span> { data: updateData, error } = <span class="hljs-keyword">await</span> supabase
                .from(<span class="hljs-string">"scores"</span>)
                .update({
                    attempts: [
                        ...data[<span class="hljs-number">0</span>].attempts,
                        { score: userScore, date: getCurrentDate() },
                    ],
                    total_score: data[<span class="hljs-number">0</span>].total_score + userScore,
                })
                .eq(<span class="hljs-string">"user_id"</span>, userID);
            <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.log(err);
    }
};
</code></pre>
<h3 id="heading-how-to-retrieve-data-from-supabase">How to retrieve data from Supabase</h3>
<p>Recall that you need to rank the users based on their scores on the Leaderboard screen and retrieve the user's attempts on the Profile screen.</p>
<p>The code snippet below accepts a user's ID and retrieves the attempts and total score from the database.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getUserAttempts = <span class="hljs-keyword">async</span> (userID: <span class="hljs-built_in">string</span>) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"attempts, total_score"</span>)
            .eq(<span class="hljs-string">"user_id"</span>, userID);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> { attempts: data[<span class="hljs-number">0</span>].attempts, total_score: data[<span class="hljs-number">0</span>].total_score };
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> { attempts: <span class="hljs-string">""</span>, total_score: <span class="hljs-number">0</span> };
    }
};
</code></pre>
<p>The code snippet below retrieves the top ten users from the database based on their score.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> getLeaderBoard = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
            .from(<span class="hljs-string">"scores"</span>)
            .select(<span class="hljs-string">"total_score, user_id"</span>)
            .order(<span class="hljs-string">"total_score"</span>, { ascending: <span class="hljs-literal">false</span> })
            .limit(<span class="hljs-number">10</span>);
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
        <span class="hljs-keyword">return</span> data;
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
};
</code></pre>
<p>Congratulations! You've successfully completed the project for this tutorial.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you’ve learned how to:</p>
<ul>
<li>build React Native mobile applications with Expo,</li>
<li>style your mobile applications with <a target="_blank" href="https://www.nativewind.dev/">Tailwind CSS</a>,</li>
<li>create stack and tab screen navigations using <a target="_blank" href="https://docs.expo.dev/router/introduction/">Expo Router</a>,</li>
<li>use Supabase and leverage its authentication and database features to build full-stack applications.</li>
</ul>
<p>Supabase is an amazing tool that enables you to build a full-stack software application with no hassle. If you are looking forward to shipping great software products or side projects faster, consider using Supabase.</p>
<p>Expo also saves us from the complexities of setting up and developing mobile applications using the <a target="_blank" href="https://reactnative.dev/docs/environment-setup">React Native CLI</a>. It enables you to focus more on building your applications while it handles the necessary configurations, including deployment.</p>
<p>Feel free to customise the application using <a target="_blank" href="https://chat.openai.com/">ChatGPT</a> to generate questions and answers tailored to any niche or topic.</p>
<p>The source code for this tutorial is available in this <a target="_blank" href="https://github.com/dha-stix/techtest-app">GitHub repository</a>.</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up Authentication in Your Apps with Supabase Auth ]]>
                </title>
                <description>
                    <![CDATA[ In this article, you'll learn the basic key concepts that'll help you grasp how authentication and authorization work.  You'll start by learning what authentication and authorization are, and then learn how to implement authentication in your applica... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/set-up-authentication-in-apps-with-supabase/</link>
                <guid isPermaLink="false">66b999acd9d170feecefbbc7</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ authorization ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Fatuma Abdullahi ]]>
                </dc:creator>
                <pubDate>Mon, 29 Jan 2024 10:53:38 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2024/01/Group-3--9-.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this article, you'll learn the basic key concepts that'll help you grasp how authentication and authorization work. </p>
<p>You'll start by learning what authentication and authorization are, and then learn how to implement authentication in your applications using Supabase auth.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
    <li>
        <a href="#prerequisites">
            Prerequisites
        </a>
    </li>
    <li>
        <a href="#what-is-authentication-and-authorization">
            What Is Authentication and Authorization?
        </a>
    </li>
    <li>
        <a href="#how-does-authentication-work">
            How Does Authentication Work?
        </a>
    </li>
    <li>
        <a href="#session-management-with-tokens-secrets-and-cookies">
            Session Management with Tokens, Secrets, and Cookies
        </a>
    </li>
    <li>
        <a href="#types-of-authentication-factors">
            Types of Authentication Factors
        </a>
    </li>
    <li>
        <a href="#common-authentication-strategies">
            Common Authentication Strategies
        </a>
        <ul>
            <li>
                <a href="#password-based-authentication">
                     Password-based Authentication
                </a>
            </li>
            <li>
                <a href="#password-less-authentication">
                     Password-less Authentication
                </a>
            </li>
            <li>
                <a href="#two-factor-authentication-2fa-">
                     Two-Factor Authentication (2FA)
                </a>
            </li>
            <li>
                <a href="#multi-factor-authentication-mfa-">
                     Multi-Factor Authentication (MFA)
                </a>
            </li>
              <li>
                <a href="#oauth-2-0-and-social-authentication">
                    OAuth 2.0 and Social Authentication
                </a>
            </li>
              <li>
                <a href="#sso-and-saml">
                    SSO and SAML
                </a>
            </li>
        </ul>
    </li>
    <li>
        <a href="#authentication-and-security">
            Authentication and Security
        </a>
    </li>
    <li>
        <a href="#supabase-and-supabase-authentication-service">
            Supabase and Supabase Authentication Service
        </a>
    </li>
    <li>
        <a href="#how-to-use-supabase-auth">
            How to use Supabase Auth
        </a>
        <ul>
            <li>
                <a href="#the-api">
                    The API
                </a>
            </li>
            <li>
                <a href="#sdks">
                    SDKs
                </a>
            </li>
            <li>
                <a href="#auth-ui-helpers">
                    Auth UI Helpers
                </a>
            </li>
        </ul>

<p>    </p></li>
     <li>
         <a href="#summary">
             Summary
         </a>
    </li>
    <li>
        <a href="#resources">
            Resources
        </a>
    </li>
</ul><p></p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You'll need the following to make the most out of this article:</p>
<ul>
<li>Basic programming knowledge.</li>
<li><a target="_blank" href="https://supabase.com/">A Supabase project</a> to follow along.</li>
<li>And a text editor to try out the example code snippets.</li>
</ul>
<h2 id="heading-what-is-authentication-and-authorization">What Is Authentication and Authorization?</h2>
<p>In simple terms, authentication is the process of a user identifying themselves to a system and the system confirming that the user is who they claim to be.   </p>
<p>On the other hand, authorization is the process of the system determining which parts of the application the user is allowed to view or interact with, and which parts the user is not allowed to access. </p>
<h2 id="heading-how-does-authentication-work">How Does Authentication Work?</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/image-135.png" alt="Image" width="600" height="400" loading="lazy">
<em>A flowchart depicting the user authentication process</em></p>
<p>The first time a user interacts with a system, they will be requested to register. Typically, the user will provide a piece of information and a secret that is meant to be known only by them and the system. This is the registration part of the authentication process.</p>
<p>The next time the user interacts with the same system, they will be required to provide the identifying information along with the previously defined secret in order to verify their identity. </p>
<p>The device the user initiates the interaction from is the client and the system is the server. Once the system verifies the user, it sends over some information to the client about the user. </p>
<p>Because this process takes time and requires some action from the user, the client will store this information and send it back to the system whenever the user needs to access the system. This reduces friction by not requiring the user to actively re-authenticate every single time. This creates a user session.</p>
<h2 id="heading-session-management-with-tokens-secrets-and-cookies">Session Management with Tokens, Secrets, and Cookies</h2>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/image-136.png" alt="Image" width="600" height="400" loading="lazy">
<em>A sequence diagram showing session management in a client-server architecture</em></p>
<p>The server can pass the user's information to the client in two ways – through tokens or session ids.</p>
<p>In the case of tokens, the server generates a signed token and passes it to the client. This token is typically a JWT and may contain information regarding the user. The client will store this token and send it back to the server every time the user makes a request. </p>
<p>The server is able to verify the integrity of the token because it signed it. This is referred to as stateless authentication because the token is self contained and the server does not need to store session data in a database or cache. </p>
<p>In the case of cookies, the server creates a record of the user session in a database or a cache that will include a session ID. The server send this session ID to the client.</p>
<p>The client stores this session ID in a cookie and sends it back to the server whenever the user makes a request. The session ID is a random string that acts as pointer to the actual user record in the database. </p>
<p>When the server receives this cookie, it matches the session ID it contains to its session records, and then matches that record to the user data in the database. This is referred to as stateful authentication because a database look-up is needed.</p>
<h2 id="heading-types-of-authentication-factors">Types of Authentication Factors</h2>
<p>An authentication factor refers to a type of credential that can be used to verify a user's identity. There are 3 factors typically used in the authentication process, and they are:</p>
<ol>
<li>Something you know: an examples is a password.</li>
<li>Something you have: an example is a token sent to your phone.</li>
<li>Something you are: an example is your fingerprint.</li>
</ol>
<h2 id="heading-common-authentication-strategies">Common Authentication Strategies</h2>
<p>Authentication strategies refer to the processes used to verify a user. Different types of authentication strategies include:</p>
<h3 id="heading-password-based-authentication">Password-based Authentication</h3>
<p>This refers to the traditional way of users identifying themselves by providing a text-based secret that is user defined. Typically, the system handles the entire process on its servers and is responsible for security and reliability.</p>
<h3 id="heading-password-less-authentication">Password-less Authentication</h3>
<p>In this approach, the system verifies the user’s identity without requiring user defined passwords. The system will, instead, generate a one time password (OTP) and send to the user. This OTP is then used in place of a password to gain access to the system. Examples include magic links, where the system sends a code to the user’s email.</p>
<h3 id="heading-two-factor-authentication-2fa">Two-Factor Authentication (2FA)</h3>
<p>The system attempts to verify the user is who they claim to be by requiring an extra piece of information after the primary authentication has checked out. </p>
<p>This can be an OTP sent to the user via email or SMS, or it can be by requiring the users' biometric information before the system grants access.</p>
<h3 id="heading-multi-factor-authentication-mfa">Multi-Factor Authentication (MFA)</h3>
<p>This is similar to 2FA, except that the system will use more than one extra method to verify the user’s identity. The extra methods or factors used in both MFA and 2FA are usually external to the system, such as an SMS requiring a phone.</p>
<h3 id="heading-oauth-20-and-social-authentication">OAuth 2.0 and Social Authentication</h3>
<p>OAuth is an authorization framework that allows clients to access information from an external server on the user's behalf. The external server prompts the user for permission to share the requested resources with the client. </p>
<p>After user permits the action, the external server issues an access token to the client. </p>
<p>The client then gives this access token to the original server, which verifies the token's validity and manages access to the requested resources. OAuth 2.0 is the latest version of OAuth and is the more widely used framework. </p>
<p>OAuth 2.0 extends support for non-browser based systems. Social Authentication is based on OAuth 2.0 but in this case, the external server that the client redirects the user to is typically a social media platform. This is the type of authentication process carried out whenever you see a "Continue with Twitter/X" button on an authentication page. </p>
<h3 id="heading-sso-and-saml">SSO and SAML</h3>
<p>SAML stands for Security Assertion Markup Language. It is a standard for passing authentication and authorization information between systems. One system acts as the requesting system or the service provider (SP) and the other system holds the requested information or acts as the Identity Provider (IdP). </p>
<p>On receiving this request, the identity provider will generate some statements in SAML form that contains some user information. The service provider then uses this information to decide how to handle the user in relation with its protected resources. </p>
<p>SSO refers to Single Sign On. This is an authentication strategy that lets users sign in through one system/application that then lets them access multiple applications within the same network. </p>
<p>This improves the user experience by not requiring the user to log in to different related applications. An example of this is Google workspace. You don't need to log into Docs separately if you are already logged into your Gmail account. SSO is facilitated by SAML as SAML provides a standard authentication mechanism and allows different systems to trust each other. </p>
<h2 id="heading-authentication-and-security">Authentication and Security</h2>
<p>Authentication involves handling, moving, and storing sensitive user information in relation to protected server resources. This makes security and best practices an important aspect of an authentication system.</p>
<p>There are some basic steps you can take to greatly increase the security of your authentication systems. These include:</p>
<ul>
<li>Enforcing stronger passwords.</li>
<li>Requiring the user to register an extra factor to enable 2FA.</li>
<li>Encrypting sensitive data as it is being transferred via HTTPS.</li>
<li>Storing passwords in an encrypted manner.</li>
<li>Using standard authentication frameworks like OAuth 2.0.</li>
</ul>
<p>There are certain compliances that your system should consider when handling sensitive user data beyond specific authentication information. This is even more so if operating in certain countries or handling enterprise applications. These compliances include:</p>
<ul>
<li><strong>GDPR</strong>: This compliance enforces standards around data handling including confidentiality and integrity.</li>
<li><strong>HIPAA</strong>: This compliance applies to medical data. It expects high levels of integrity.</li>
<li><strong>SOC</strong>: This is a framework more generally required of cloud technologies. It is based on the American Institute of CPAs and covers aspects of privacy, security, availability, integrity and confidentiality.</li>
</ul>
<p>Keeping all this in mind, you will find that it is often easier to use dedicated authentication services for your applications instead of rolling out your own auth.</p>
<p>There are lots of options for this, including dedicated authentication services such as Clerk and Auth0, and Backend-as-a-Service such as Supabase and Firebase. In this case, let's take a look at the Supabase authentication offering.</p>
<h2 id="heading-supabase-and-supabase-authentication-service">Supabase and Supabase Authentication Service</h2>
<p>Supabase is an open source Backend as a Service (BaaS) platform that makes developing a backend for your applications very easy and fast. It is based on open source technologies and actively supports the open source ecosystem. </p>
<p>Supabase offers common services that most backend applications will require. These services are:</p>
<ul>
<li>Database: This is a fully featured Postgres database.</li>
<li>Authentication: This is an enterprise ready authentication service that is based on a fork of the goTrue server.</li>
<li>Realtime: This is an API that adds the ability to use real-time capabilities in your applications.</li>
<li>Storage: This is a storage service which is an s3 wrapper.</li>
<li>Edge Functions: These are serverless functions that run on the edge. Powered by the Deno runtime.</li>
<li>Vector: This is a vector database that makes it easier to work with embeddings in your AI applications.</li>
</ul>
<p>Supabase is SOC2, HIPAA and GDPR compliant, self-host-able and open source. Furthermore, their authentication service exposes many strategies, giving you full control over your data and can be used independently of their other offerings. This and their auto documenting API makes it a very good choice for your applications. </p>
<h3 id="heading-how-to-use-supabase-auth">How to use Supabase Auth</h3>
<p>The first step is to set up your <a target="_blank" href="https://app.supabase.com/">Supabase project</a>'s auth settings. You can enable the exact authentication methods you want to use via the settings. There are three ways you can start using Supabase auth in your project:</p>
<h4 id="heading-the-api">The API</h4>
<p>You can directly use the authentication service in your applications by calling the auth endpoint and passing the user information to it. You can also get, update and delete your users.   </p>
<p>The API is automatically available when you create a project via the Supabase console and can be called like so: </p>
<pre><code class="lang-javascript"><span class="hljs-comment">//This will return an object containing an access token, the newly created user data and other metadata</span>
<span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://&lt;your-project-ref&gt;/auth/v1/signup"</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
  <span class="hljs-attr">headers</span>: {
    <span class="hljs-attr">authorization</span>: <span class="hljs-string">"Bearer YOUR_SUPABASE_KEY"</span>,
    <span class="hljs-string">"content-type"</span>: <span class="hljs-string">"application/json"</span>,
  },
  <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
    <span class="hljs-attr">email</span>: <span class="hljs-string">"user-email"</span>,
    <span class="hljs-attr">password</span>: <span class="hljs-string">"user-password"</span>,
  }),
});
</code></pre>
<h4 id="heading-sdks">SDKs</h4>
<p>Supabase offers a few SDKs (software development kits) in different programming languages meant to make interacting with your Supabase project straightforward. Languages officially supported include Dart and JavaScript, with Python and others having strong community support. </p>
<p>The procedure for getting started involves adding the SDK as a dependency, then connecting your application to your Supabase project. </p>
<p>In the case of the JavaScript SDK, this would look something like this:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">//Install via npm:</span>
npm install @supabase/supabase-js

<span class="hljs-comment">// or add cdn links: </span>
&lt;script src=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/@supabase/supabase-js@2"</span>&gt;&lt;/script&gt;
</code></pre>
<pre><code class="lang-javascript"><span class="hljs-comment">//Then initialize </span>
Supabaseimport { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>

<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-string">'https://&lt;your-project-ref&gt;.supabase.co'</span>
<span class="hljs-keyword">const</span> supabaseKey = process.env.SUPABASE_ANON_KEY
<span class="hljs-keyword">const</span> supabase = createClient(supabaseUrl, supabaseKey)
</code></pre>
<p>Then you can access the authentication methods under the auth namespace like so:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
  <span class="hljs-attr">email</span>: <span class="hljs-string">'user email'</span>,
  <span class="hljs-attr">password</span>: <span class="hljs-string">'user password'</span>,
})
</code></pre>
<h4 id="heading-auth-ui-helpers">Auth UI Helpers</h4>
<p>Supabase provides helper libraries to make authentication using their service even easier. These libraries provide customizable login screens that support magic links, password based and social login strategies. </p>
<p>Currently, the libraries are available for JavaScript and Flutter. Supabase also provides a separate SSR (Server Side Rendering) package for applications that use server side frameworks and require a Supabase access token to be available to them.</p>
<p>To start using React Auth UI, as an example, first you need to install the dependencies as shown below:</p>
<pre><code class="lang-bash">npm install @supabase/supabase-js @supabase/auth-ui-react 
@supabase/auth-ui-shared
</code></pre>
<p>Then you can start using the library after initializing Supabase as in the SDK example above. Here is some sample code that shows how to use the auth UI library in a React application:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">"react"</span>;
<span class="hljs-keyword">import</span> { useNavigate } <span class="hljs-keyword">from</span> <span class="hljs-string">"react-router-dom"</span>;
<span class="hljs-keyword">import</span> { Auth } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/auth-ui-react"</span>;
<span class="hljs-keyword">import</span> { ThemeSupa } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/auth-ui-shared"</span>;
<span class="hljs-keyword">import</span> { supa } <span class="hljs-keyword">from</span> <span class="hljs-string">"../constants"</span>;

<span class="hljs-keyword">const</span> AuthUi = <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">const</span> navigate = useNavigate();

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> {
      <span class="hljs-attr">data</span>: { subscription },
    } = supa.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event === <span class="hljs-string">"SIGNED_IN"</span>) {
        navigate(<span class="hljs-string">"/authenticated"</span>);
      }
    });

    <span class="hljs-keyword">return</span> <span class="hljs-function">() =&gt;</span> subscription.unsubscribe();
  }, [navigate]);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Auth</span>
      <span class="hljs-attr">supabaseClient</span>=<span class="hljs-string">{supa}</span>
      <span class="hljs-attr">providers</span>=<span class="hljs-string">{[</span>"<span class="hljs-attr">google</span>", "<span class="hljs-attr">github</span>", "<span class="hljs-attr">apple</span>", "<span class="hljs-attr">discord</span>"]}
      // <span class="hljs-attr">controls</span> <span class="hljs-attr">whether</span> <span class="hljs-attr">to</span> <span class="hljs-attr">display</span> <span class="hljs-attr">only</span> <span class="hljs-attr">social</span> <span class="hljs-attr">providers</span>
      // <span class="hljs-attr">onlyThirdPartyProviders</span>
      <span class="hljs-attr">redirectTo</span>=<span class="hljs-string">"http://localhost:3000/authenticated"</span>
      // <span class="hljs-attr">comes</span> <span class="hljs-attr">with</span> <span class="hljs-attr">preconfigured</span> <span class="hljs-attr">themes</span>, <span class="hljs-attr">can</span> <span class="hljs-attr">add</span> <span class="hljs-attr">custom</span> <span class="hljs-attr">themes</span>
      <span class="hljs-attr">appearance</span>=<span class="hljs-string">{{</span> <span class="hljs-attr">theme:</span> <span class="hljs-attr">ThemeSupa</span> }}
      // <span class="hljs-attr">controls</span> <span class="hljs-attr">how</span> <span class="hljs-attr">to</span> <span class="hljs-attr">display</span> <span class="hljs-attr">the</span> <span class="hljs-attr">social</span> <span class="hljs-attr">provider</span> <span class="hljs-attr">icons</span>
      <span class="hljs-attr">socialLayout</span>=<span class="hljs-string">"horizontal"</span>
    /&gt;</span></span>
  );
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> AuthUi;
</code></pre>
<p>This would display the following form on screen: </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2024/01/Screenshot-2024-01-26-at-18.43.39.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-summary">Summary</h2>
<p>Authentication is the process of the user identifying themselves and the server verifying this identity while authorization is the system determining what access the user should have to the resources and limiting the user accordingly.</p>
<p>After the server authenticates the user, it will pass the user information in the form of either a token or session ID within a cookie. </p>
<p>The information will be passed back and forth between the client and server whenever the user needs certain access until they expire or the user terminates the cycle by logging out or deleting their account.</p>
<p>This process of user verification occurs by employing certain factors of authentication. For example, one system may only require a password while another requires a password and a code sent to the users phone number.</p>
<p>Your authentication system can allow multiple strategies of authentication using any of the three auth factors.</p>
<p>Supabase is an excellent option if you opt not to handle your own auth.</p>
<p>Supabase auth can be accessed via the API, the SDKs and the Auth libraries. Supabase maintains an SSR package for server side frameworks.</p>
<h2 id="heading-resources">Resources</h2>
<p>The following resources are helpful further reading. They offer more explanations on authentication and authorization, as well as Supabase specific documentation.</p>
<ul>
<li><a target="_blank" href="https://www.upguard.com/blog/oauth#:~:text=OAuth%201.0%20has%20a%20consumer,resource%20server%2C%20and%20resource%20owner.">An indepth explanation of OAuth</a></li>
<li><a target="_blank" href="https://supabase.com/security">Supabase security</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth">Supabase docs on authentication</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/auth-helpers/auth-ui">Auth UI docs page</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/auth-helpers">Supabase on auth helpers and SSR</a></li>
<li><a target="_blank" href="https://supabase.com/docs/guides/auth/sso/auth-sso-saml">On SSO, SAML and enterprise auth</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Full Stack Development with Next.js and Supabase by Building a Twitter Clone. ]]>
                </title>
                <description>
                    <![CDATA[ Turn this: Into this: We just launched a course on the freeCodeCamp.org YouTube channel that will help you master full stack development with Next.js 13 and Supabase by building a Twitter clone. The course also uses Tailwind CSS and TypeScript. Thi... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-full-stack-development-with-next-js-and-supabase-by-building-a-twitter-clone/</link>
                <guid isPermaLink="false">66b2044a39b555ffda8bfe9f</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 20 Jul 2023 15:49:43 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/07/twitter-clone.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Turn this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-133.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Into this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2023/07/image-134.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We just launched a course on the freeCodeCamp.org YouTube channel that will help you master full stack development with Next.js 13 and Supabase by building a Twitter clone. The course also uses Tailwind CSS and TypeScript.</p>
<p>This intensive course has been expertly developed by Pranjal Soni, who brings a wealth of experience and a passion for teaching complex coding concepts in an accessible and understandable way.</p>
<p>The course is designed around a project-based learning approach, providing you with the opportunity to gain hands-on experience as you create a functional, full-stack Twitter clone. In addition to building your practical skills, the project will also serve as a powerful portfolio piece that can showcase your ability to develop and manage a complex full-stack project.</p>
<p>Here's a brief rundown of what you can expect from the course:</p>
<p><strong>Twitter Intro &amp; Building the Ultimate Twitter Clone</strong>: Get introduced to the project and dive right into the exciting task of creating a Twitter clone using Next.js 13, Tailwind CSS, TypeScript, and Supabase.</p>
<p><strong>Building Twitter Layouts</strong>: Learn how to develop the visually appealing layouts that make Twitter so user-friendly, from the left sidebar to the timeline and the trending section.</p>
<p><strong>Designing Twitter Clone PostgreSQL Database</strong>: Harness the power of ChatGPT and dbdiagram to design a PostgreSQL database tailored for a Twitter-like application.</p>
<p><strong>Authentication with Supabase</strong>: Set up user authentication in your Next.js 13 application using Supabase.</p>
<p><strong>Handling Tweets</strong>: Master how to save, fetch, and manage tweets in your application's database using Next.js Server Actions and Supabase.</p>
<p><strong>Implementing Tweet Features</strong>: From a Like/Unlike feature to a Tweet Reply function, you'll gain practical experience in adding the essential features that Twitter users know and love.</p>
<p><strong>Optimization and Debugging</strong>: Learn about optimizing your web application for better performance and experience debugging by fixing a real bug in your Twitter clone.</p>
<p><strong>Drizzle ORM Schema and Complex Queries</strong>: Use ChatGPT to build a Drizzle ORM schema from SQL and learn how to join tables and build complex queries using Drizzle ORM.</p>
<p>By the end of the course, you'll have a firm grasp on full-stack development, with practical experience in Next.js 13, Tailwind CSS, TypeScript, and Supabase. Not only will you have your own Twitter clone as proof of your new skills, but you'll also be well-equipped to take on your own full-stack projects in the future.</p>
<p>The course is entirely free, reflecting freeCodeCamp.org's commitment to providing accessible and high-quality coding education to all. So why wait? </p>
<p>Watch the full course on <a target="_blank" href="https://youtu.be/QdxUZhLHZiA">the freeCodeCamp.org YouTube channel</a> (6-hour watch).</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/QdxUZhLHZiA" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Learn Supabase, an Open-Source Firebase Alternative ]]>
                </title>
                <description>
                    <![CDATA[ Supabase is an open-source Firebase alternative that is rapidly gaining popularity in the developer community. If you are looking to sharpen their backend skills, consider learning how to use Supabase. We just published a course on the freeCodeCamp.o... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/learn-supabase-open-source-firebase-alternative/</link>
                <guid isPermaLink="false">66b2051243f24c1bb1598173</guid>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ youtube ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Beau Carnes ]]>
                </dc:creator>
                <pubDate>Thu, 22 Jun 2023 12:20:35 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2023/06/supabase.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Supabase is an open-source Firebase alternative that is rapidly gaining popularity in the developer community. If you are looking to sharpen their backend skills, consider learning how to use Supabase.</p>
<p>We just published a course on the freeCodeCamp.org channel that is designed to help you master Supabase.</p>
<p>This course was created by Guillaume Duhan. With his extensive experience and dedication to imparting knowledge, Guillaume has broken down complex concepts into easily digestible parts, ensuring that learners of all levels can follow along and build a solid foundation.</p>
<h2 id="heading-what-is-supabase">What is Supabase?</h2>
<p>Supabase is an incredible tool for building secure and high-performance Postgres backends with minimal configuration. It provides developers with a wide array of functionalities similar to Firebase, such as authentication, real-time database, and storage. However, as an open-source alternative, Supabase grants you greater flexibility and control over your data and applications.</p>
<h2 id="heading-delve-into-the-course-content">Delve into the Course Content 🌟</h2>
<p>The course is structured into various sections, allowing you to progressively build your knowledge of Supabase.</p>
<ul>
<li><strong>Introduction</strong>: Get an overview of Supabase and its potential as an alternative to Firebase.</li>
<li><strong>Create a Project</strong>: Learn how to create and set up a new Supabase project.</li>
<li><strong>Tables</strong>: Understand the concept of tables within Supabase and how to work with them.</li>
<li><strong>RLS (Row Level Security)</strong>: Dive into securing your data through Row Level Security.</li>
<li><strong>Authentication &amp; User Management</strong>: Explore how to authenticate users and manage user accounts in your application.</li>
<li><strong>Recover Password &amp; E-mails Templates</strong>: Learn how to handle password recovery and customize email templates for your application.</li>
<li><strong>URL Configuration</strong>: Understand how to configure URLs for various purposes within your project.</li>
<li><strong>Read, Insert or Delete</strong>: Grasp the basic CRUD operations in Supabase.</li>
<li><strong>Subscriptions &amp; Relations</strong>: Discover how to create real-time subscriptions and define relationships between tables.</li>
<li><strong>Functions, Triggers &amp; Schemas</strong>: Learn to create custom functions, set triggers, and define schemas in your database.</li>
<li><strong>Storage &amp; Logs</strong>: Understand how to store files and monitor logs within your Supabase application.</li>
<li><strong>Extensions, Realtime &amp; Edge-functions</strong>: Enhance your application with extensions, real-time capabilities, and edge functions.</li>
<li><strong>Webhooks</strong>: Discover how to integrate webhooks to connect your Supabase application with other services.</li>
<li><strong>Self-host or Local Development</strong>: Finally, learn the options for deploying your application, either through self-hosting or using local development environments.</li>
</ul>
<h2 id="heading-who-is-this-course-for">Who is this Course for?</h2>
<p>This course is perfect for beginners looking to dip their toes into backend development, as well as seasoned developers seeking to learn a new tool. Whether you're building a simple app or an intricate web system, understanding Supabase will add an invaluable skill to your toolkit.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>The Supabase course by Guillaume Duhan on freeCodeCamp.org’s YouTube channel is a treasure trove for anyone seeking to excel in web development.</p>
<p>Head over to <a target="_blank" href="https://www.youtube.com/watch?v=dU7GwCOgvNY">freeCodeCamp.org's YouTube channel</a> to start learning now!</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/dU7GwCOgvNY" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Podcast Player with Transcriptions using Vue and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ In this post we will walk through setting up a Podcast Player app using Supabase and Vue 3, including getting transcriptions for the podcasts.  This is a continuation of my previous post on setting up Authentication using Supabase. If you aren't fami... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-podcast-player-with-transcriptions-using-vue-supabase/</link>
                <guid isPermaLink="false">66bb92ddd2bda3e4315491ed</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ projects ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Brian Barrow ]]>
                </dc:creator>
                <pubDate>Mon, 28 Feb 2022 23:05:43 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/02/Build-Podcast-Player-app-w-transcriptions-using-Vue-Supabase@2x.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this post we will walk through setting up a Podcast Player app using Supabase and Vue 3, including getting transcriptions for the podcasts. </p>
<p>This is a continuation of my previous post on <a target="_blank" href="https://www.freecodecamp.org/news/add-supabase-authentication-to-vue/">setting up Authentication using Supabase</a>. If you aren't familiar with getting Supabase set up in your project, I highly recommend going through that post. </p>
<h2 id="heading-the-starting-code-repo">The Starting Code Repo</h2>
<p>Here is the repo from my previous post that will get you to where this post will be starting. You'll just need to set up Supabase and add your credentials/API key to a <code>.env.local</code> file to get up and running. This repo also has styling applied to it that was not included in the previous post.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/briancbarrow/vue-supabase-auth">https://github.com/briancbarrow/vue-supabase-auth</a></div>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be familiar with JavaScript, have had some experience with Vue 3, and you should have Node.js and NPM installed on your machine. </p>
<p>If you've gone through the previous post about Supabase Authentication or this other post on <a target="_blank" href="https://developers.deepgram.com/blog/2021/11/getting-started-with-supabase/">Getting Started with Supabase</a> you'll be good to go.</p>
<p>You will also need a <a target="_blank" href="https://console.deepgram.com/signup">free API key from Deepgram</a> for when we get to the transcription section. </p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Once you have downloaded the <a target="_blank" href="https://github.com/briancbarrow/vue-supabase-auth">repo from above</a> run <code>npm install</code> to get the packages installed for the project. </p>
<p>Add your <code>VITE_SUPABASE_URL</code> and <code>VITE_SUPABASE_ANON_KEY</code> environment variables from your the dashboard of your own Supabase project.</p>
<p>Run <code>npm run dev</code> to get the local dev server started.</p>
<p>Sign in to the app using either the Sign In form or the Magic Link form. Once you get signed in, you should see the HelloWorld component/page with a sign out button at the top. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-2.20.26-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Hello World component</em></p>
<h2 id="heading-how-to-fetch-a-podcast-rss-feed">How to Fetch a Podcast RSS Feed</h2>
<p>The first thing we need to do is add functionality to get a podcast feed into our app. Create a new component in the components folder called <code>PodcastFeed.vue</code>. </p>
<p>Most podcasts have a public RSS feed that we can use to get the information we need with a simple fetch request. </p>
<p>Inside the of the <code>PodcastFeed.vue</code> component create the following form that takes in a RSS feed URL, and hooks up to a button that triggers the fetch request.</p>
<p>Note: I've tried to add comments in the code to help you understand what each part is doing.</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-input-feed"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Podcast RSS Feed URL<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- binding the url input field to the 'url' data property --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"https://rss.your-org.org/feed/"</span>
        <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"rss-url"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- hooking the button click to the 'getRssFeed' method --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"getRssFeed()"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Get Feed<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-comment">// I am initializing the url to a url I know works, so that I don't need to keep inputing a url as I'm developing.</span>
    <span class="hljs-comment">// feel free to change this to a url of your own choosing</span>
    <span class="hljs-keyword">const</span> url = ref(<span class="hljs-string">"https://anchor.fm/s/3e9db190/podcast/rss"</span>);
    <span class="hljs-comment">// initializing the podcast state to an empty object</span>
    <span class="hljs-keyword">const</span> podcast = ref({});

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRssFeed</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> feedUrl = url.value;
      <span class="hljs-keyword">return</span> (
        fetch(feedUrl)
          <span class="hljs-comment">// this returns a promise so we need to convert it to a string</span>
          .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
          <span class="hljs-comment">// this next line is to parse the xml response</span>
          .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span>
            <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>)
          )
          <span class="hljs-comment">// parsing the data from the xml response and setting it into the podcast state</span>
          .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Data: "</span>, data);
            podcast.value.image_url = data
              .querySelector(<span class="hljs-string">"image"</span>)
              .querySelector(<span class="hljs-string">"url"</span>).innerHTML;
            podcast.value.title = data.querySelector(<span class="hljs-string">"title"</span>).textContent;
            podcast.value.description =
              data.querySelector(<span class="hljs-string">"description"</span>).textContent;
            podcast.value.rss_url = feedUrl;
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"ERROR: "</span>, err);
          })
      );
    }
    <span class="hljs-keyword">return</span> {
      url,
      podcast,
      store,

      getRssFeed,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>With that set up, replace the HelloWorld component in the <code>App.vue</code> file with this new <code>PodcastFeed.vue</code> component:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"store.state.user"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"signout-button"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"signOut"</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  &lt;!-- Check <span class="hljs-keyword">if</span> user is available <span class="hljs-keyword">in</span> the store, <span class="hljs-keyword">if</span> not show auth compoenent --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Auth</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!store.state.user"</span> /&gt;</span></span>
  &lt;!-- If user is available, show the app --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"app"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PodcastFeed</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> Auth <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Auth.vue"</span>;
<span class="hljs-keyword">import</span> PodcastFeed <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastFeed.vue"</span>;

<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"./store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    PodcastFeed,
    Auth,
  },
  setup() {
    <span class="hljs-comment">// we initially verify if a user is logged in with Supabase</span>
    store.state.user = supabase.auth.user();
    <span class="hljs-comment">// we then set up a listener to update the store when the user changes either by logging in or out</span>
    supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event == <span class="hljs-string">"SIGNED_OUT"</span>) {
        store.state.user = <span class="hljs-literal">null</span>;
      } <span class="hljs-keyword">else</span> {
        store.state.user = session.user;
      }
    });

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signOut</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
    }

    <span class="hljs-keyword">return</span> {
      store,

      signOut,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>So now the app should look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-2.38.40-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>App after adding PodcastFeed.vue</em></p>
<p>When you click the button, the data that comes back from the fetch request will show up in the console. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/xml-data-from-rss.png" alt="Image" width="600" height="400" loading="lazy">
<em>Parsed XML Data</em></p>
<p>In the <code>getRssFeed</code> method we parse that data and then take the information we need and add it to the <code>podcast</code> state data. We need to display that data so that the user can know the request was successful. We also want to add better error messaging in case the request fails. </p>
<p>Create a new component called <code>PodcastInfo.vue</code> and add the following code:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-info"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">""</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-text"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title-desc"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title"</span>&gt;</span>
          {{ podcast.title }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"desc"</span>&gt;</span>
          {{ podcast.description }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">podcast</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  <span class="hljs-attr">computed</span>: {},
  <span class="hljs-attr">methods</span>: {},
  setup() {},
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info-error"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>There was an error with your request<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Check your RSS feed URL and try again.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Then update the  <code>PodcastFeed.vue</code> to the following in order to bring in the component:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-input-feed"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Podcast RSS Feed URL<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- binding the url input field to the 'url' data property --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"https://rss.your-org.org/feed/"</span>
        <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"rss-url"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- hooking the button click to the 'getRssFeed' method --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"getRssFeed()"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Get Feed<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Adding in these two new components --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">podcast-info</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"podcast.title &amp;&amp; !requestError"</span> <span class="hljs-attr">:podcast</span>=<span class="hljs-string">"podcast"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;

<span class="hljs-keyword">import</span> PodcastInfo <span class="hljs-keyword">from</span> <span class="hljs-string">"./PodcastInfo.vue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    PodcastInfo,
  },

  setup() {
    <span class="hljs-comment">// I am initializing the url to a url I know works, so that I don't need to keep inputing a url as I'm developing.</span>
    <span class="hljs-comment">// feel free to change this to a url of your own choosing</span>
    <span class="hljs-keyword">const</span> url = ref(<span class="hljs-string">"https://anchor.fm/s/3e9db190/podcast/rss"</span>);
    <span class="hljs-comment">// initializing the podcast state to an empty object</span>
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> requestError = ref(<span class="hljs-literal">false</span>);

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRssFeed</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> feedUrl = url.value;
      <span class="hljs-keyword">return</span> (
        fetch(feedUrl)
          <span class="hljs-comment">// this returns a promise so we need to convert it to a string</span>
          .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
          <span class="hljs-comment">// this next line is to parse the xml response</span>
          .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span>
            <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>)
          )
          <span class="hljs-comment">// parsing the data from the xml response and setting it into the podcast state</span>
          .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Data: "</span>, data);
            podcast.value.image_url = data
              .querySelector(<span class="hljs-string">"image"</span>)
              .querySelector(<span class="hljs-string">"url"</span>).innerHTML;
            podcast.value.title = data.querySelector(<span class="hljs-string">"title"</span>).textContent;
            podcast.value.description =
              data.querySelector(<span class="hljs-string">"description"</span>).textContent;
            podcast.value.rss_url = feedUrl;
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            requestError.value = <span class="hljs-literal">true</span>;
          })
      );
    }
    <span class="hljs-keyword">return</span> {
      url,
      podcast,
      store,
      requestError,

      getRssFeed,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Now when you click the "Get Feed" button, you should see the following:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-3.05.15-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now that we can get the info displayed, we can wire up the app to save the info to Supabase. </p>
<h2 id="heading-how-to-add-a-table-to-the-supabase-db">How to Add a Table to the Supabase DB</h2>
<p>The first thing we need to do is add a table to our Supabase DB. In the Dashboard for your project on Supabase, select the Table Editor and click the <code>New table</code> button. I name the new table <code>podcasts</code>. Enable the Row Level Security (this makes our DB more secure) and add the following columns:</p>
<ul>
<li>id (this column should be filled in for you when you create a new table)</li>
<li>created_at</li>
<li>name</li>
<li>image_url</li>
<li>description</li>
<li>rss_url</li>
<li>user_id (for this one, we want to link it via foreign key to our users table created by the Auth service. Click the chain-link icon to get that set up and link it to the <code>users</code> table on the <code>id</code> column.)</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-01-19-at-9.08.50-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>podcasts table setup</em></p>
<p>Because we enabled the Row Level Security, the table won't let anything get inserted until we update the policies for it. </p>
<p>Under the Authentication tab there is a section called 'Policies'. There you should see the <code>podcasts</code> table and a button to create a new policy. When you click on it, it will give you the option to create a policy from a template. Choose the template called 'Enable insert access for authenticated users only'. Now, only users that are authenticated have access to insert anything into the table. </p>
<p>When Supabase runs the <code>insert</code> command, it will automatically run a <code>select</code> command and return the newly inserted row. Because of this, we also have to add a policy to the table allowing the user <code>SELECT</code> access. </p>
<p>Create a new policy with the name <code>Enable select based on userid</code> and then in the <code>USING expression</code> section put <code>(uid() = user_id)</code>. That will prevent users from reading other users' information, while still giving them access to their own podcasts in the table.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/select-user-policy.png" alt="Image" width="600" height="400" loading="lazy">
<em>Select based on userid policy</em></p>
<h2 id="heading-how-to-link-up-the-ui-to-the-db-so-the-user-can-save-podcasts">How to Link Up the UI to the DB So the User Can Save Podcasts</h2>
<p>To add a podcast to our DB, we will first add a button to the <code>PodcastInfo</code> component. Add this code to the bottom of the <code>&lt;div class="podcast-info"&gt;</code>:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"addPodcast"</span>&gt;</span>Add to My Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>Now add a method called <code>addPodcast</code> to the setup function of the component like this. Don't forget to add the <code>props</code> to the argument of the setup function.</p>
<pre><code class="lang-js">setup(props) {
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPodcast</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-comment">// Setting up the podcast object to send to supabase</span>
      <span class="hljs-keyword">const</span> podcast = {
        <span class="hljs-attr">name</span>: props.podcast.title,
        <span class="hljs-attr">image_url</span>: props.podcast.image_url,
        <span class="hljs-attr">description</span>: props.podcast.description,
        <span class="hljs-attr">rss_url</span>: props.podcast.rss_url,
        <span class="hljs-attr">user_id</span>: store.state.user.id,
      };
      <span class="hljs-comment">// calling supabase method to insert into the db</span>
      supabase
        .from(<span class="hljs-string">"podcasts"</span>)
        .insert(podcast)
        .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
          store.addPodcastToStore(body[<span class="hljs-number">0</span>]);
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(err);
        });
    }

    <span class="hljs-keyword">return</span> {
      addPodcast,
    };
  },
</code></pre>
<p>You can see in the <code>.then</code> statement we call a method from the global store. Update the <code>store.js</code> file to the following:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { reactive } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> store = {
  <span class="hljs-attr">state</span>: reactive({
    <span class="hljs-attr">user</span>: {},
    <span class="hljs-comment">// adding podcasts array to global store</span>
    <span class="hljs-attr">podcasts</span>: [],
  }),

  <span class="hljs-comment">// adding addPodcastToStore method to store object</span>
  addPodcastToStore(podcast) {
    <span class="hljs-built_in">this</span>.state.podcasts.push(podcast);
  },
};
</code></pre>
<p>Now when we click the "Add To My Podcasts" button, the app makes a call to Supabase and then takes the result of that call and adds it to the podcasts array in the global store. (If you are getting a 403 error, make sure you set up the policies correctly. Maybe try restarting the dev server too.)</p>
<p>If a podcast is already in a user's list of podcasts, we don't want to let them click the add button again. To prevent this we need to first call Supabase to get all of the user's podcasts, and then check if the podcast they are looking at is in that list.</p>
<p>This method will not be specific to any one component so we want to create it inside of the global store. That way, any component has access to it. Add this method to the <code>store.js</code> file underneath the <code>addPodcastToStore</code> method:</p>
<pre><code class="lang-js">getPodcastsFromDB() {
    supabase
        .from(<span class="hljs-string">"podcasts"</span>)
        .select(<span class="hljs-string">"*"</span>)
        .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.state.podcasts = body;
        });
},
</code></pre>
<p>Then we want to update the method to be called whenever a user signs in. Inside of <code>App.vue</code> change the <code>onAuthStateChange</code> handler to this:</p>
<pre><code class="lang-js">supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (event == <span class="hljs-string">"SIGNED_OUT"</span>) {
        store.state.user = <span class="hljs-literal">null</span>;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// make call to supabase to get Podcasts for the user</span>
        store.getPodcastsFromDB();
        store.state.user = session.user;
    }
});
</code></pre>
<p>Now update the <code>PodcastInfo.vue</code> file to this in order to display to the user if a podcast is already in their library.</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-info"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">""</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-text"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title-desc"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title"</span>&gt;</span>
          {{ podcast.title }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"desc"</span>&gt;</span>
          {{ podcast.description }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Add check in markup to remove the button if the podcast already exists in the user's list --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"isInUserPodcasts"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"in-podcasts"</span>&gt;</span>In Your Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"addPodcast"</span>&gt;</span>Add to My Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// importing computed</span>
<span class="hljs-keyword">import</span> { ref, computed } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">podcast</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  setup(props) {
    <span class="hljs-comment">// add computed property checking if podcast is in user's podcasts</span>
    <span class="hljs-keyword">const</span> isInUserPodcasts = computed(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">return</span> store.state.podcasts.some(
        <span class="hljs-function">(<span class="hljs-params">podcast</span>) =&gt;</span> podcast.rss_url === props.podcast.rss_url
      );
    });

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPodcast</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-comment">// check if podcast is already in user's podcasts</span>
      <span class="hljs-keyword">if</span> (isInUserPodcasts.value) {
        alert(<span class="hljs-string">"You already have this podcast in your list!"</span>);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> podcast = {
          <span class="hljs-attr">name</span>: props.podcast.title,
          <span class="hljs-attr">image_url</span>: props.podcast.image_url,
          <span class="hljs-attr">description</span>: props.podcast.description,
          <span class="hljs-attr">rss_url</span>: props.podcast.rss_url,
          <span class="hljs-attr">user_id</span>: store.state.user.id,
        };
        supabase
          .from(<span class="hljs-string">"podcasts"</span>)
          .insert(podcast)
          .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
            store.addPodcastToStore(body[<span class="hljs-number">0</span>]);
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err);
          });
      }
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-comment">// exposing the isInUserPodcasts computed property</span>
      isInUserPodcasts,
      addPodcast,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>Next, we want to display a list of podcasts that the user has added to their library. We have the list in the global store, so we just need to loop through them to display the needed info. </p>
<p>Add the following to the bottom of the <code>PodcastFeed.vue</code> template:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- Loop through podcasts and display them --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"feeds"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Your Podcast Feeds<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">v-for</span>=<span class="hljs-string">"pod in store.state.podcasts"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"pod.id"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">:href</span>=<span class="hljs-string">"`/podcast/${pod.id}`"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"pod.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"`logo for ${pod.name}`"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ pod.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<h2 id="heading-how-to-set-up-the-other-pages-for-our-podcast-app">How to Set Up the Other Pages for Our Podcast App</h2>
<p>Now that we have the list of podcasts being displayed in the app, we need a way to navigate to an individual podcast. We have the markup set to link to a path like <code>/podcast/{podcast_id}</code>. Now we need to update our app to handle routes like this. </p>
<p>First, install vue-router using <code>npm i vue-router</code>.</p>
<p>Then create a file called <code>router.js</code> with the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">// Import Vue Router</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> VueRouter <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;

<span class="hljs-comment">// Import the components that will show on the different routes</span>
<span class="hljs-keyword">import</span> PodcastFeed <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastFeed.vue"</span>;
<span class="hljs-keyword">import</span> PodcastDetail <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastDetail.vue"</span>;

<span class="hljs-comment">// Set up the routes</span>
<span class="hljs-keyword">const</span> routes = [
  { <span class="hljs-attr">path</span>: <span class="hljs-string">"/"</span>, <span class="hljs-attr">component</span>: PodcastFeed },
  { <span class="hljs-attr">path</span>: <span class="hljs-string">"/podcast/:id"</span>, <span class="hljs-attr">component</span>: PodcastDetail },
];

<span class="hljs-comment">// Initialize the router</span>
<span class="hljs-keyword">const</span> router = VueRouter.createRouter({
  <span class="hljs-attr">history</span>: VueRouter.createWebHistory(),
  routes,
});

<span class="hljs-comment">// Export the router</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>Update <code>main.js</code> to use the router in the Vue app:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { createApp } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> router <span class="hljs-keyword">from</span> <span class="hljs-string">"./router"</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App.vue"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./index.css"</span>;

<span class="hljs-keyword">const</span> app = createApp(App);
app.use(router);
app.mount(<span class="hljs-string">"#app"</span>);
</code></pre>
<p>Update <code>App.vue</code> to show the <code>router-view</code> component provided by Vue Router:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"store.state.user"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"signout-button"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"signOut"</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Check if user is available in the store, if not show auth compoenent --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">Auth</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!store.state.user"</span> /&gt;</span>
  <span class="hljs-comment">&lt;!-- If user is available, show the app --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"app"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">router-view</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">router-view</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>Now create a <code>PodcastDetail.vue</code> file that will display the episode info for the podcast:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
  &lt;!-- Basic layout <span class="hljs-keyword">for</span> showing podcast info --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>          
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>        
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// Importing necessary methods</span>
<span class="hljs-keyword">import</span> { ref, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { useRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> route = useRoute();
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> episodes = ref([]);

    <span class="hljs-comment">// Getting podcast info from the database</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPodcastData</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> {
        <span class="hljs-attr">data</span>: [podcastinfo],
      } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"podcasts"</span>).select().eq(<span class="hljs-string">"id"</span>, route.params.id);
      podcast.value = podcastinfo;

      <span class="hljs-comment">// Making call to episode url to get episode info</span>
      getEpisodes(podcastinfo.rss_url);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getEpisodes</span>(<span class="hljs-params">url</span>) </span>{
      fetch(url)
        .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
        .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>))
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
          <span class="hljs-comment">// Finding all the "item" tags in the xml response which will contain the episode info</span>
          <span class="hljs-keyword">const</span> items = data.querySelectorAll(<span class="hljs-string">"item"</span>);
          <span class="hljs-comment">// Looping through each item and getting the episode info and pushing it to the 'episodes' array</span>
          items.forEach(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
            <span class="hljs-keyword">let</span> url;

            <span class="hljs-comment">// Not every podcast episode is going to have the `enclosure` tag, so we need to check if it exists</span>
            <span class="hljs-keyword">try</span> {
              url = item.querySelector(<span class="hljs-string">"enclosure"</span>).getAttribute(<span class="hljs-string">"url"</span>);
            } <span class="hljs-keyword">catch</span> (e) {
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"error"</span>, e);
              url = item.querySelector(<span class="hljs-string">"link"</span>).innerHTML;
            }

            episodes.value.push({
              <span class="hljs-comment">// this `title` and the `guid` properties looks a little different because the title contains CDATA tags which need to be grabbed with the 'childNodes' property</span>
              <span class="hljs-attr">title</span>: item.querySelector(<span class="hljs-string">"title"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
              <span class="hljs-attr">link</span>: url,
              <span class="hljs-attr">url</span>: url,
              <span class="hljs-attr">description</span>: item.querySelector(<span class="hljs-string">"description"</span>).innerHTML,
              <span class="hljs-attr">pubDate</span>: item.querySelector(<span class="hljs-string">"pubDate"</span>).innerHTML,
              <span class="hljs-attr">guid</span>: item.querySelector(<span class="hljs-string">"guid"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
            });
          });
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          alert(<span class="hljs-string">"Couldn't get episodes"</span>, err);
        });
    }

    onMounted(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Getting podcast info from the database once the component is mounted</span>
      getPodcastData();
    });

    <span class="hljs-keyword">return</span> {
      podcast,
      episodes,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>With those changes, we can now see the individual episodes of the podcast and can play them using the <code>&lt;audio&gt;</code> html tag. </p>
<h2 id="heading-how-to-get-the-transcriptions-of-the-podcasts">How to Get the Transcriptions of the Podcasts</h2>
<p>The last step is to get transcriptions for the podcasts, and then save them to our database.</p>
<p>If you haven't yet, you'll need to get a <a target="_blank" href="https://console.deepgram.com/signup">free API key from Deepgram</a> in order to process the audio and get the transcriptions. </p>
<p>Once you get the API key, add it to your <code>.env.local</code> file as <code>VITE_DEEPGRAM_KEY</code>. Make sure you restart your dev server here, otherwise you'll probably get a 403 Forbidden error when we finally call the API.</p>
<p>Then add this code to a deepgram.js file in the <code>src</code> folder.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> deepgramKey = <span class="hljs-keyword">import</span>.meta.env.VITE_DEEPGRAM_KEY;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deepgram</span>(<span class="hljs-params">url</span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
    <span class="hljs-string">"https://api.deepgram.com/v1/listen?punctuate=true&amp;diarize=true&amp;utterances=true"</span>,
    {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-attr">Authorization</span>: <span class="hljs-string">`Token <span class="hljs-subst">${deepgramKey}</span>`</span>,
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      },
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">url</span>: url,
      }),
    }
  );
  <span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> response.json();
  <span class="hljs-keyword">return</span> json.results;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> deepgram;
</code></pre>
<p>This gives us a utility function we can import into our app in other files to call the Deepgram API in order to get the transcriptions. We added the punctuate, diarize, and utterances to the URL as parameters to get a cleaner transcription that is easier to read. </p>
<p>Now that we have that, we need to add some functionality to the <code>PodcastDetail.vue</code> file. I'll walk through the changes and then later will put up the final code for the file.</p>
<p>First we need to have some state to keep track of the transcriptions we get, and also to have some loading state once we click a button to get a transcription. So we'll add these two lines to our setup function:</p>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> transcriptions = ref({});
<span class="hljs-keyword">const</span> episodeTranscriptionLoading = ref([]);
</code></pre>
<p>Don't forget to add them to the return object of the <code>setup</code> function.</p>
<p>Then add this function to make the request to Deepgram, and then add the transcription to the local <code>transcriptions</code> object.</p>
<pre><code class="lang-js"><span class="hljs-comment">// Function to get a transcription from Deepgram, passing in the episode url</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscription</span>(<span class="hljs-params">episode</span>) </span>{
    <span class="hljs-comment">// setting the loading state to true for the episode</span>
    episodeTranscriptionLoading.value.push(episode.guid);
    <span class="hljs-keyword">const</span> transcription = <span class="hljs-keyword">await</span> deepgram(episode.url);
    <span class="hljs-comment">// setting a unique id for the episode transcription</span>
    transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcast.value.id}</span>---<span class="hljs-subst">${episode.guid}</span>`</span>] =
        transcription;
    <span class="hljs-comment">// removing the loading state for the episode</span>
    episodeTranscriptionLoading.value.splice(
        episodeTranscriptionLoading.value.indexOf(episode.guid),
        <span class="hljs-number">1</span>
    );
}
</code></pre>
<p>Make sure you import the deepgram function from <code>deepgram.js</code> at the top of the script tag.</p>
<p>Then update the template to this:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Basic layout for showing podcast info --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>
            <span class="hljs-comment">&lt;!-- button to get transcriptions --&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!transcriptions[`${podcast.id}---${episode.guid}`]"</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"getTranscription(episode)"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
          &gt;</span>
            {{
              episodeTranscriptionLoading.includes(episode.guid)
                ? "Loading..."
                : "Get Transcription"
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-comment">&lt;!-- box to display the transcription --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-if</span>=<span class="hljs-string">"transcriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{
              transcriptions[`${podcast.id}---${episode.guid}`].channels[0]
                .alternatives[0].transcript
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> 
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<h2 id="heading-how-to-save-the-transcriptions">How to Save the Transcriptions</h2>
<p>Now that we can get the transcriptions, we need to add the functionality to save them to Supabase. </p>
<p>First, go create a table in Supabase like we did above, but this time name the table <code>transcriptions</code>. You'll want the following as the columns:</p>
<ul>
<li><strong>id</strong> – varchar (primary) Also remove the "Is Identity" check mark in the settings for this column</li>
<li><strong>podcast_id</strong> – int8</li>
<li><strong>episode_guid</strong> – varchar</li>
<li><strong>transcript</strong> – text</li>
<li><strong>user_id</strong> – uuid (You'll need to link this to the user table by clicking on the link icon)</li>
<li><strong>created_at</strong> – timestamptz</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-22-at-4.40.52-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>columns for transcriptions table</em></p>
<p>Once that table is set up, we can add a reactive property called <code>savedTranscriptions</code> to the component and then add the following code to save the transcriptions to Supabase. Then we'll store them in the object <code>savedTranscriptions</code>.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveTranscription</span>(<span class="hljs-params">podcastId, episodeGuid</span>) </span>{
    supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .insert({
        <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>,
        <span class="hljs-attr">podcast_id</span>: podcastId,
        <span class="hljs-attr">episode_guid</span>: episodeGuid,
        <span class="hljs-attr">transcript</span>:
        transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>].channels[<span class="hljs-number">0</span>]
        .alternatives[<span class="hljs-number">0</span>].transcript,
        <span class="hljs-attr">user_id</span>: store.state.user.id,
    })
        .then(<span class="hljs-function">(<span class="hljs-params">{ data: [transcriptObject] }</span>) =&gt;</span> {
        savedTranscriptions.value[transcriptObject.id] =
            transcriptObject.transcript;
    });
}
</code></pre>
<p>Once a user has a transcription saved, we need to display it whenever they re-visit a page. Add this function to get that data from Supabase:</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscriptions</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: transcriptions } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">"transcriptions"</span>)
    .select()
    .eq(<span class="hljs-string">"podcast_id"</span>, podcast.value.id);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Transcriptions"</span>, transcriptions);
    transcriptions.forEach(<span class="hljs-function">(<span class="hljs-params">transcript</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"id"</span>, transcript.id);
        savedTranscriptions.value[transcript.id] = transcript.transcript;
    });
}
</code></pre>
<p>We will want this called whenever a user hits this page, but not until we get the podcast information. Add a call to <code>getTranscriptions</code> to the bottom of the <code>getPodcastData</code> function to do that.</p>
<p>The last thing to do is to update the template to include the save buttons and to display transcriptions if they are in the saved objects. The final code for the <code>PodcastDetail.vue</code> should then look like this:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
  &lt;!-- Basic layout <span class="hljs-keyword">for</span> showing podcast info --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"savedTranscriptions[`${podcast.id}---${episode.guid}`]"</span>
            <span class="hljs-attr">disabled</span>
          &gt;</span>
            Transcription Saved
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"
              !transcriptions[`${podcast.id}---${episode.guid}`] &amp;&amp;
              !savedTranscriptions[`${podcast.id}---${episode.guid}`]
            "</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"getTranscription(episode)"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
          &gt;</span>
            {{
              episodeTranscriptionLoading.includes(episode.guid)
                ? "Loading..."
                : "Get Transcription"
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"
              transcriptions[`${podcast.id}---${episode.guid}`] &amp;&amp;
              !savedTranscriptions[`${podcast.id}---${episode.guid}`]
            "</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"save"</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"saveTranscription(podcast.id, episode.guid)"</span>
          &gt;</span>
            Save Transcription
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-if</span>=<span class="hljs-string">"savedTranscriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{ savedTranscriptions[`${podcast.id}---${episode.guid}`] }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"transcriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{
              transcriptions[`${podcast.id}---${episode.guid}`].channels[0]
                .alternatives[0].transcript
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// Importing necessary methods</span>
<span class="hljs-keyword">import</span> { ref, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { useRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> deepgram <span class="hljs-keyword">from</span> <span class="hljs-string">"../deepgram"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> route = useRoute();
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> episodes = ref([]);
    <span class="hljs-keyword">let</span> transcriptions = ref({});
    <span class="hljs-keyword">let</span> savedTranscriptions = ref({});
    <span class="hljs-keyword">const</span> episodeTranscriptionLoading = ref([]);

    <span class="hljs-comment">// Getting podcast info from the database</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPodcastData</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> {
        <span class="hljs-attr">data</span>: [podcastinfo],
      } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"podcasts"</span>).select().eq(<span class="hljs-string">"id"</span>, route.params.id);
      podcast.value = podcastinfo;

      <span class="hljs-comment">// Making call to episode url to get episode info</span>
      getEpisodes(podcastinfo.rss_url);
      <span class="hljs-keyword">await</span> getTranscriptions();
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getEpisodes</span>(<span class="hljs-params">url</span>) </span>{
      fetch(url)
        .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
        .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>))
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
          <span class="hljs-comment">// Finding all the "item" tags in the xml response which will contain the episode info</span>
          <span class="hljs-keyword">const</span> items = data.querySelectorAll(<span class="hljs-string">"item"</span>);
          <span class="hljs-comment">// Looping through each item and getting the episode info and pushing it to the 'episodes' array</span>
          items.forEach(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
            <span class="hljs-keyword">let</span> url;

            <span class="hljs-comment">// Not every podcast episode is going to have the `enclosure` tag, so we need to check if it exists</span>
            <span class="hljs-keyword">try</span> {
              url = item.querySelector(<span class="hljs-string">"enclosure"</span>).getAttribute(<span class="hljs-string">"url"</span>);
            } <span class="hljs-keyword">catch</span> (e) {
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"error"</span>, e);
              url = item.querySelector(<span class="hljs-string">"link"</span>).innerHTML;
            }

            episodes.value.push({
              <span class="hljs-comment">// this `title` and the `guid` properties looks a little different because the title contains CDATA tags which need to be grabbed with the 'childNodes' property</span>
              <span class="hljs-attr">title</span>: item.querySelector(<span class="hljs-string">"title"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
              <span class="hljs-attr">link</span>: url,
              <span class="hljs-attr">url</span>: url,
              <span class="hljs-attr">description</span>: item.querySelector(<span class="hljs-string">"description"</span>).innerHTML,
              <span class="hljs-attr">pubDate</span>: item.querySelector(<span class="hljs-string">"pubDate"</span>).innerHTML,
              <span class="hljs-attr">guid</span>: item.querySelector(<span class="hljs-string">"guid"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
            });
          });
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          alert(<span class="hljs-string">"Couldn't get episodes"</span>, err);
        });
    }

    <span class="hljs-comment">// Function to get a transcription from Deepgram, passing in the episode url</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscription</span>(<span class="hljs-params">episode</span>) </span>{
      <span class="hljs-comment">// setting the loading state to true for the episode</span>
      episodeTranscriptionLoading.value.push(episode.guid);
      <span class="hljs-keyword">const</span> transcription = <span class="hljs-keyword">await</span> deepgram(episode.url);
      <span class="hljs-comment">// setting a unique id for the episode transcription</span>
      transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcast.value.id}</span>---<span class="hljs-subst">${episode.guid}</span>`</span>] =
        transcription;
      <span class="hljs-comment">// removing the loading state for the episode</span>
      episodeTranscriptionLoading.value.splice(
        episodeTranscriptionLoading.value.indexOf(episode.guid),
        <span class="hljs-number">1</span>
      );
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveTranscription</span>(<span class="hljs-params">podcastId, episodeGuid</span>) </span>{
      <span class="hljs-built_in">console</span>.log(
        <span class="hljs-string">"saving transcription"</span>,
        transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>]
      );
      supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .insert({
          <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>,
          <span class="hljs-attr">podcast_id</span>: podcastId,
          <span class="hljs-attr">episode_guid</span>: episodeGuid,
          <span class="hljs-attr">transcript</span>:
            transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>].channels[<span class="hljs-number">0</span>]
              .alternatives[<span class="hljs-number">0</span>].transcript,
          <span class="hljs-attr">user_id</span>: store.state.user.id,
        })
        .then(<span class="hljs-function">(<span class="hljs-params">{ data: [transcriptObject] }</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"saved"</span>, transcriptObject);
          savedTranscriptions.value[transcriptObject.id] =
            transcriptObject.transcript;
        });
    }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscriptions</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: transcriptions } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .select()
        .eq(<span class="hljs-string">"podcast_id"</span>, podcast.value.id);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Transcriptions"</span>, transcriptions);
      transcriptions.forEach(<span class="hljs-function">(<span class="hljs-params">transcript</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"id"</span>, transcript.id);
        savedTranscriptions.value[transcript.id] = transcript.transcript;
      });
    }

    onMounted(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Getting podcast info from the database once the component is mounted</span>
      getPodcastData();
    });

    <span class="hljs-keyword">return</span> {
      podcast,
      episodes,
      transcriptions,
      savedTranscriptions,
      episodeTranscriptionLoading,

      getTranscription,
      saveTranscription,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you've followed the steps above, you should have a working app. <a target="_blank" href="https://github.com/briancbarrow/vue-supabase-auth/tree/final-podcast-feed-transcriptions">Here is the final code</a> if you want to double check against what I have written.</p>
<p>I know I enjoyed making the app. Supabase makes it really easy to get a database/backend up and running. Please reach out to me on <a target="_blank" href="https://twitter.com/the_BrianB">Twitter</a> if you have any questions!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Add Supabase Authentication to a Vue App ]]>
                </title>
                <description>
                    <![CDATA[ In this post we will walk through getting authentication set up using Supabase and Vue 3.  This will also work with Vue 2, but you'll need to move things around to work with the options API. I am using Vue 3 as it is now the default version.  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/add-supabase-authentication-to-vue/</link>
                <guid isPermaLink="false">66bb92dbadd24ba42732510d</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Brian Barrow ]]>
                </dc:creator>
                <pubDate>Thu, 10 Feb 2022 16:40:00 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/02/Getting-Started-with-supabase-blog@2x.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this post we will walk through getting authentication set up using Supabase and Vue 3. </p>
<p>This will also work with Vue 2, but you'll need to move things around to work with the options API. I am using Vue 3 as it is <a target="_blank" href="https://blog.vuejs.org/posts/vue-3-as-the-new-default.html">now the default version</a>. </p>
<p>Just a heads up – there will not be much styling involved here so that the focus stays on Supabase authentication. </p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be familiar with JavaScript and have had some experience with Vue 3. Having some experience with Supabase will help, but isn't necessary. </p>
<p>If you need a quick review of Supabase you can check out <a target="_blank" href="https://developers.deepgram.com/blog/2021/11/getting-started-with-supabase/">a previous post</a> of mine where I go over how to get started using it. </p>
<p>You will also need <a target="_blank" href="https://nodejs.org/en/">Node.js</a> and NPM installed on your machine.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Let's start by building some of the frontend before we start building the database using Supabase. </p>
<p>The first thing we need to do is set up our project. Inside of your terminal and inside of the folder where you want this project to live, run this command:</p>
<pre><code class="lang-bash">npm init vite@latest vue-supabase-auth --template vue
</code></pre>
<p>This will initialize a new Vite project with Vue 3 in a folder called <code>vue-supabase-auth</code>. </p>
<p>Open this up in your code editor of choice and open up the <code>App.vue</code> file inside of the <code>src</code> folder. When I initialized the project, Vite put the script tag above the template tag. My personal preference is to move the template tag to the top, but that isn't necessary.</p>
<h2 id="heading-add-authentication-to-the-app">Add Authentication to the App</h2>
<p>The next step is to add authentication to our app. Supabase gives us the ability to authenticate a user in many different ways. </p>
<p>We'll be walking through how to set up basic email/password authentication, and authentication with a "magic link". A "magic link" is just a link sent to a user's email that, when clicked, will take them to your application and log them in. </p>
<h3 id="heading-get-a-supabase-account">Get a Supabase Account</h3>
<p>If you haven't already, you'll need to sign up for an account on <a target="_blank" href="https://app.supabase.io">Supabase</a>. It asks you to sign up with GitHub, so if you don't have a GitHub account, you should also sign up for one of those.</p>
<p>Once you are signed in, you'll click the green button that says "New Project" and select the default organization that was created when you logged in. Mine was called "briancbarrow's Org." </p>
<p>This will bring up a box where you provide some info about the project. I'll name it <code>basic-auth</code>, give it a strong password, and then I'm going to select the region <code>West US (North California)</code> because that is closest to me.</p>
<p>Once the project is done setting up, go into the Authentication -&gt; Settings and disable the "Enable email confirmations." It just makes things a little smoother for this tutorial.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/01/disable-email-confirmations.png" alt="Image" width="600" height="400" loading="lazy">
<em>Showing the "Enable email confirmations" setting disabled</em></p>
<h3 id="heading-set-up-supabase-in-the-vue-project">Set up Supabase in the Vue Project</h3>
<p>First, we need to run <code>npm install @supabase/supabase-js</code> to get the JavaScript package to integrate with Supabase.</p>
<p>Then we need to create a <code>supabase.js</code> file in the <code>src</code> folder of the project. That should contain the following:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@supabase/supabase-js'</span>

<span class="hljs-keyword">const</span> supabaseUrl = <span class="hljs-keyword">import</span>.meta.env.VITE_SUPABASE_URL
<span class="hljs-keyword">const</span> supabaseAnonKey = <span class="hljs-keyword">import</span>.meta.env.VITE_SUPABASE_ANON_KEY

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> supabase = createClient(supabaseUrl, supabaseAnonKey)
</code></pre>
<p>As you can see in the code above, we need to set up some environment variables that contain our Supabase keys. Create a <code>.env.local</code> file at the root of the project and add the <code>VITE_SUPABASE_URL</code> and the <code>VITE_SUPABASE_ANON_KEY</code>. You can find your url and anon_key on the dashboard of your Supabase project. </p>
<p>Your <code>.env.local</code> file will look like this:</p>
<pre><code>VITE_SUPABASE_URL=YOUR_SUPABASE_URL
VITE_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
</code></pre><p>We also want to create a central store for data that is needed throughout the app, like user info. Create a <code>store.js</code> file in the <code>src</code> folder and fill it with this code:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { reactive } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> store = {
  <span class="hljs-attr">state</span>: reactive({
    <span class="hljs-attr">user</span>: {},
  }),
};
</code></pre>
<h3 id="heading-create-signin-and-signup-components">Create SignIn and SignUp Components</h3>
<p>Supabase authentication separates the <code>signIn</code> and <code>signUp</code> processes, so we'll need to handle them differently. I decided to create two separate components just to make things a little clearer in my head.</p>
<p>Create a <code>SignUp.vue</code> file in the components folder and add the following code:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Sign up for an account<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> @<span class="hljs-attr">submit.prevent</span>=<span class="hljs-string">"handleSignup"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"password"</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"password"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Sign up<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> email = ref(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> password = ref(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">const</span> handleSignup = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// Use the Supabase provided method to handle the signup</span>
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signUp({
          <span class="hljs-attr">email</span>: email.value,
          <span class="hljs-attr">password</span>: password.value,
        });
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
      } <span class="hljs-keyword">catch</span> (error) {
        alert(error.error_description || error.message);
      }
    };

    <span class="hljs-keyword">return</span> {
      email,
      password,
      handleSignup,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Now create a <code>SignIn.vue</code> file and add the code below. The only differences are the method names that get called, and the text is slightly different in the markup.</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Sign in to your account<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> @<span class="hljs-attr">submit.prevent</span>=<span class="hljs-string">"handleSignin"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"password"</span>&gt;</span>Password<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"password"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"password"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Sign in<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> email = ref(<span class="hljs-string">""</span>);
    <span class="hljs-keyword">const</span> password = ref(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">const</span> handleSignin = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// Use the Supabase provided method to handle the signin</span>
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signIn({
          <span class="hljs-attr">email</span>: email.value,
          <span class="hljs-attr">password</span>: password.value,
        });
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
      } <span class="hljs-keyword">catch</span> (error) {
        alert(error.error_description || error.message);
      }
    };

    <span class="hljs-keyword">return</span> {
      email,
      password,
      handleSignin,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Now we want to create a wrapper component for these two. Create a file named <code>Auth.vue</code> with the code below:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">sign-up</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"isSignUp"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">sign-in</span> <span class="hljs-attr">v-else</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"isSignUp = !isSignUp"</span>&gt;</span>
      {{
        isSignUp
          ? "Already have an account? Sign In"
          : "Don't have an account yet? Sign Up"
      }}
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> SignUp <span class="hljs-keyword">from</span> <span class="hljs-string">"./SignUp.vue"</span>;
<span class="hljs-keyword">import</span> SignIn <span class="hljs-keyword">from</span> <span class="hljs-string">"./SignIn.vue"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: { SignUp, SignIn },
  setup() {
    <span class="hljs-keyword">const</span> isSignUp = ref(<span class="hljs-literal">true</span>);

    <span class="hljs-keyword">return</span> {
      isSignUp,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>This just allows the user to toggle between the <code>SignIn</code> and <code>SignUp</code> views. Now open up <code>App.vue</code> again and update the code to this:</p>
<pre><code class="lang-js">&lt;template&gt;
  &lt;!-- Check <span class="hljs-keyword">if</span> user is available <span class="hljs-keyword">in</span> the store, <span class="hljs-keyword">if</span> not show auth compoenent --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Auth</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!store.state.user"</span> /&gt;</span></span>
  &lt;!-- If user is available, show the Hello World component --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">HelloWorld</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">msg</span>=<span class="hljs-string">"Hello Vue 3 + Vite"</span> /&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> Auth <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Auth.vue"</span>;
<span class="hljs-keyword">import</span> HelloWorld <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/HelloWorld.vue"</span>;

<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"./store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    HelloWorld,
    Auth,
  },
  setup() {
    <span class="hljs-comment">// we initially verify if a user is logged in with Supabase</span>
    store.state.user = supabase.auth.user();
    <span class="hljs-comment">// we then set up a listener to update the store when the user changes either by logging in or out</span>
    supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event == <span class="hljs-string">"SIGNED_OUT"</span>) {
        store.state.user = <span class="hljs-literal">null</span>;
      } <span class="hljs-keyword">else</span> {
        store.state.user = session.user;
      }
    });

    <span class="hljs-keyword">return</span> {
      store,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="css">
<span class="hljs-selector-id">#app</span> {
  <span class="hljs-attribute">font-family</span>: Avenir, Helvetica, Arial, sans-serif;
  <span class="hljs-attribute">-webkit-font-smoothing</span>: antialiased;
  <span class="hljs-attribute">-moz-osx-font-smoothing</span>: grayscale;
  <span class="hljs-attribute">text-align</span>: center;
  <span class="hljs-attribute">color</span>: <span class="hljs-number">#2c3e50</span>;
  <span class="hljs-attribute">margin-top</span>: <span class="hljs-number">60px</span>;
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>This will show the <code>Auth</code> component if a user is not signed in, otherwise it will show the <code>HelloWorld.vue</code> as we had initially set up.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-09-at-10.14.27-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Sign up form</em></p>
<p>Sign up using your email and a password you create, and you should then see the HelloWorld component again.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-09-at-10.18.58-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Hello World component showing after sign up</em></p>
<h3 id="heading-how-to-log-out">How to log out</h3>
<p>Logging out is relatively straightforward. Inside the HelloWorld component, add the following to the bottom of the template tag:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"signOut"</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>Then update the script tag on HelloWorld to this:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">setup</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

defineProps({
  <span class="hljs-attr">msg</span>: <span class="hljs-built_in">String</span>,
});

<span class="hljs-keyword">const</span> count = ref(<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">signOut</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>You can see we are now importing the <code>supabase</code> file we created earlier and then creating a <code>signOut</code> method that gets called on a button click. </p>
<h3 id="heading-authenticate-using-magic-link">Authenticate using Magic Link</h3>
<p>Supabase also offers the ability to send users a magic link to their email which they click, and it takes them to the app and signs them in. The link it sends will redirect them to your site, so we need to make sure we have the correct redirect URL in our Supabase setup. </p>
<p>Navigate to the Auth -&gt; Settings page in the Supabase dashboard for your project and make sure the localhost URL is in the <code>Site URL</code> box. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-09-at-10.24.13-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Make sure your site url matches where it should redirect to on login</em></p>
<h4 id="heading-create-the-magiclink-component">Create the MagicLink component</h4>
<p>Create a new file in the components folder called <code>MagicLink.vue</code> and add the following code:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span>&gt;</span>Sign in With Magic Link<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">form</span> @<span class="hljs-attr">submit.prevent</span>=<span class="hljs-string">"handleMagicLink"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Email<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span> /&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"submit"</span>&gt;</span>Sign in<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">form</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> email = ref(<span class="hljs-string">""</span>);

    <span class="hljs-keyword">const</span> handleMagicLink = <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">try</span> {
        <span class="hljs-comment">// We call the signIn method from Supabase to send the magic link. We only pass it the email though.</span>
        <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signIn({
          <span class="hljs-attr">email</span>: email.value,
        });
        <span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">throw</span> error;
      } <span class="hljs-keyword">catch</span> (error) {
        alert(error.error_description || error.message);
      }
    };

    <span class="hljs-keyword">return</span> {
      email,
      handleMagicLink,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>This component is very similar to the <code>SignIn</code> component. It uses the same method, but to get the magic link, we only pass in the email. </p>
<p>Now we need to update <code>Auth.vue</code> to also use the <code>MagicLink</code> component. Update <code>Auth.vue</code> to the following:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- v-if logic to determine which auth component to show --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">sign-up</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"isSignUp &amp;&amp; !useMagicLink"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">sign-in</span> <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"!isSignUp &amp;&amp; !useMagicLink"</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">magic-link</span> <span class="hljs-attr">v-else</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!useMagicLink"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!useMagicLink"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"isSignUp = !isSignUp"</span>&gt;</span>
        {{
          isSignUp
            ? "Already have an account? Sign In"
            : "Don't have an account yet? Sign Up"
        }}
      <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Or<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"toggleMagicLink"</span>&gt;</span>
      {{
        useMagicLink
          ? "Sign in with email and password"
          : "Sign in with magic link"
      }}
    <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> SignUp <span class="hljs-keyword">from</span> <span class="hljs-string">"./SignUp.vue"</span>;
<span class="hljs-keyword">import</span> SignIn <span class="hljs-keyword">from</span> <span class="hljs-string">"./SignIn.vue"</span>;
<span class="hljs-keyword">import</span> MagicLink <span class="hljs-keyword">from</span> <span class="hljs-string">"./MagicLink.vue"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: { SignUp, SignIn, MagicLink },
  setup() {
    <span class="hljs-keyword">const</span> isSignUp = ref(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">const</span> useMagicLink = ref(<span class="hljs-literal">false</span>);

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toggleMagicLink</span>(<span class="hljs-params"></span>) </span>{
      useMagicLink.value = !useMagicLink.value;
    }

    <span class="hljs-keyword">return</span> {
      isSignUp,
      useMagicLink,

      toggleMagicLink,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>Now the Auth page should look like this by default:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-09-at-10.50.35-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Default Auth view</em></p>
<p>And it should look like this if the user clicks on the "Sign in with magic link" button:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-09-at-10.50.42-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Auth view when signing in with magic link</em></p>
<p>If you input your email and click "Sign In", you should get an email with the magic link. Click that link, and you should be redirected to the app as a logged in user, where you'll see the HelloWorld view. </p>
<h2 id="heading-summary">Summary</h2>
<p>Supabase makes setting up authentication relatively easy. They also provide authentication using several social providers like Google, Apple, Github, and many more. </p>
<p>For my basic projects, I like to keep it simple and stick with email/password login or just letting Supabase send a magic link to log them in. </p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
