Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      CodeSOD: An Echo In Here in here

      September 19, 2025

      How To Minimize The Environmental Impact Of Your Website

      September 19, 2025

      Progress adds AI coding assistance to Telerik and Kendo UI libraries

      September 19, 2025

      Wasm 3.0 standard is now officially complete

      September 19, 2025

      Development Release: Ubuntu 25.10 Beta

      September 18, 2025

      Development Release: Linux Mint 7 Beta “LMDE”

      September 18, 2025

      Distribution Release: Tails 7.0

      September 18, 2025

      Distribution Release: Security Onion 2.4.180

      September 18, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      GenStudio for Performance Marketing: What’s New and What We’ve Learned

      September 19, 2025
      Recent

      GenStudio for Performance Marketing: What’s New and What We’ve Learned

      September 19, 2025

      Agentic and Generative Commerce Can Elevate CX in B2B

      September 19, 2025

      AI Momentum and Perficient’s Inclusion in Analyst Reports – Highlights From 2025 So Far

      September 18, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Denmark’s Strategic Leap Replacing Microsoft Office 365 with LibreOffice for Digital Independence

      September 19, 2025
      Recent

      Denmark’s Strategic Leap Replacing Microsoft Office 365 with LibreOffice for Digital Independence

      September 19, 2025

      Development Release: Ubuntu 25.10 Beta

      September 18, 2025

      Development Release: Linux Mint 7 Beta “LMDE”

      September 18, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»How to Build Secure SSR Authentication with Supabase, Astro, and Cloudflare Turnstile

    How to Build Secure SSR Authentication with Supabase, Astro, and Cloudflare Turnstile

    June 20, 2025

    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 link authentication using Supabase, bot protection via Cloudflare Turnstile, protected routes and middleware, and secure session management.

    Table of Contents

    • Prerequisites

    • Understanding the Technologies

      • What is Astro?

      • What are Astro Actions?

      • What is Supabase?

      • What is Cloudflare Turnstile?

    • Understanding SSR Authentication

      • SSR vs. SPA Authentication
    • Why Protect Auth Forms?

    • Part 1: How to Set Up the Backend

      • Set Up Supabase Backend

      • Set Up Cloudflare Turnstile

    • Part 2: How to Set Up the Frontend

      • Create the Astro Project

      • Configure Astro for SSR

      • Install Supabase Dependencies

      • Configure Environment Variables

    • Part 3: How to Set Up Supabase SSR

      • Create the Supabase Client

      • Create Middleware for Route Protection

    • Part 4: How to Build the User Interface

      • Update the Layout

      • Create the Sign-In Page

      • Create the Protected Page

    • Part 5: How to Set Up Astro Actions

      • Create the Authentication Actions

      • Create the Code Exchange API Route

    • Part 6: How to Test Your Application

    • Notes and Additional Resources

      • Useful Documentation

      • Complete Code Repository

    Prerequisites

    This tutorial assumes you are familiar with:

    • Web development frameworks

    • Basic authentication flows

    • Basic Backend-as-a-Service (BaaS) concepts

    Understanding the Technologies

    What is Astro?

    Astro is a UI-agnostic web framework that renders server-first by default. It can be used with any UI framework, including Astro client components.

    What are Astro Actions?

    Astro actions 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.

    What is Supabase?

    Supabase is an open-source Backend-as-a-Service that builds upon Postgres. 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.

    What is Cloudflare Turnstile?

    Turnstile is Cloudflare’s replacement for CAPTCHAs, which are visual puzzles used to differentiate between genuine users and bots. Unlike traditional CAPTCHAs, which are visually clunky, annoying, and sometimes difficult to solve, Turnstile detects malicious activity without requiring users to solve puzzles, while providing a better user experience.

    Understanding SSR Authentication

    Server-side rendered (SSR) auth refers to handling authentication on the server using a cookie-based authentication method.

    The flow works as follows:

    1. The server creates a session and stores a session ID in a cookie sent to the client

    2. The browser receives the cookie and automatically includes it in future requests

    3. The server uses the cookie to determine if the user is authenticated

    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.

    SSR vs. SPA Authentication

    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.

    Why Protect Auth Forms?

    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.

    Part 1: How to Set Up the Backend

    Set Up Supabase Backend

    First, you’ll need a Supabase account. Create a project, then:

    1. Go to the Authentication tab in the sidebar

    2. Click the Sign In / Up tab under Configuration

    3. Enable user sign-ups

    4. Scroll down to Auth Providers and enable email (disable email confirmation for this tutorial)

    Supabase authentication configuration interface showing user signup options and email provider enabled

    Set Up Cloudflare Turnstile

    1. Log in or register for a Cloudflare account

    2. Click the Turnstile tab in the sidebar

    3. Click the “Add widget” button

    4. Name your widget and add “localhost” as the hostname

    5. Leave all other settings as default, and create the widget

    Cloudflare Turnstile widget creation interface

    After creating the widget, copy the secret key and add it to your Supabase dashboard:

    1. Go back to Supabase Authentication settings

    2. Navigate to the Auth Protection tab under Configuration

    3. Turn on Captcha protection

    4. Choose Cloudflare as the provider

    5. Paste your secret key

    Supabase Attack Protection settings with Turnstile configuration

    Part 2: How to Set Up the Frontend

    Create the Astro Project

    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.

    npm create astro@latest ssr-auth
    

    Follow the provided prompts and choose a basic template to start with. When it’s done, change into the folder, then run npm install to install dependencies, followed by npm run dev to start the server, and your site will be available at localhost:4321.

    Configure Astro for SSR

    Set Astro to run in SSR mode by adding output: "server", to the defineConfig function found in the astro.config.mjs file at the root of the folder.

    Next, add an adapter to create a server runtime. For this, use the Node.js adapter by running this command in a terminal: npx astro add node. This will add it and automatically make all relevant changes.

    Finally, add Tailwind for styling. Run this command in a terminal window: npx astro add tailwind. Follow the prompts, and it will make any changes necessary.

    At this stage, your astro.config.mjs should look like this:

    <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()],
      },
    });
    

    Install Supabase Dependencies

    You can do this by running the following command:

    npm install @supabase/supabase-js @supabase/ssr
    

    Configure Environment Variables

    Create a .env file in the project root and add the following. Remember to replace with your actual credentials:

    SUPABASE_URL=<YOUR_URL>
    SUPABASE_ANON_KEY=<YOUR_ANON_KEY>
    TURNSTILE_SITE_KEY=<YOUR_TURNSTILE_SITE_KEY>
    

    You can get the Supabase values from the dashboard:

    Supabase project connection interface showing environment variables

    💡Note: 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.

    Part 3: How to Set Up Supabase SSR

    Create the Supabase Client

    Create src/lib/supabase.ts:

    
    <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>) =></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>) =></span>
                            cookies.set(name, value, options)
                        );
                    },
                },
            }
        );
    }
    

    This sets up Supabase to handle cookies in a server-rendered application 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:

    • Through Astro’s global object, which is only available on Astro pages.

    • Through AstroAPIContext object, which is only available in Astro actions.

    • Through APIContext which is a subset of the global object and is available through API routes and middleware.

    So the createClient function accepts the request and cookies objects separately to make it flexible and applicable in the various contexts in which it may be used.

    Create Middleware for Route Protection

    Next, create a middleware.ts file in the src folder and paste this into it:

    <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) => {
        <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();
    });
    

    This middleware checks for an active user when accessing the protected route and redirects unauthenticated users to the index page.

    Part 4: How to Build the User Interface

    Update the Layout

    First, update src/layouts/Layout.astro to include the Turnstile script. Add this just above the closing </head> tag:

    <script
        src=<span class="hljs-string">"https://challenges.cloudflare.com/turnstile/v0/api.js"</span>
        <span class="hljs-keyword">async</span>
        defer>
    </script>
    

    Create the Sign-In Page

    Replace the contents of src/pages/index.astro:

    ---
    <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;
    ---
    
    <Layout>
        <section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>>
            <h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>>Sign In to Your Account</h1>
            <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>>
                <label <span class="hljs-keyword">for</span>=<span class="hljs-string">"email"</span> <span class="hljs-keyword">class</span>=<span class="hljs-string">""</span>>Enter your email</label>
                <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
                />
                <div <span class="hljs-keyword">class</span>=<span class="hljs-string">"cf-turnstile"</span> data-sitekey={apiKey}></div>
                <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>
                    >Sign In</button
                >
            </form>
        </section>
    </Layout>
    

    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.

    The template displays a simple form with an email input. To complete it, add this below the closing </Layout> tag:

    
    <script>
        <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">() =></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) => {
            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>);
            }
        });
    </script>
    

    This adds some vanilla JavaScript that calls the SignIn 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.

    Create the Protected Page

    Create src/pages/protected.astro:

    ---
    <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();
    ---
    
    <Layout>
        <section <span class="hljs-keyword">class</span>=<span class="hljs-string">"flex flex-col items-center justify-center m-30"</span>>
            <h1 <span class="hljs-keyword">class</span>=<span class="hljs-string">"text-4xl text-left font-bold mb-12"</span>>You are logged <span class="hljs-keyword">in</span>!</h1>
            <p <span class="hljs-keyword">class</span>=<span class="hljs-string">"mb-6"</span>>Your user Id: {data.user?.id}</p>
            <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>
                >Sign Out</button
            >
        </section>
    </Layout>
    
    <script>
        <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) => {
            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>);
            }
        });
    </script>
    

    This page retrieves the user data server-side in the front matter and displays it in the template, along with a sign-out button.

    The JavaScript in the script tags handle calling the sign-out action, user feedback, and button state, as in the index.astro page.

    Part 5: How to Set Up Astro Actions

    Create the Authentication Actions

    Finally, add an actions folder in the src folder and create an index.ts file to hold our logic. Paste the following into it:

    <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
    ) => {
        <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) => {
                <span class="hljs-keyword">return</span> emailSignUp(input, context);
            },
        }),
        signOut: defineAction({
            handler: <span class="hljs-keyword">async</span> (_, context) => {
                <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>,
                };
            },
        }),
    };
    

    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.

    It also passes the token verification, allowing Supabase to perform verification on our behalf, eliminating the need to call Cloudflare’s verify APIs directly.

    The sign-out method calls Supabase’s sign-out method and handles any potential errors.

    The redirect URL refers to an API route that exchanges the code from the email Supabase sends for a session that Supabase handles.

    Create the Code Exchange API Route

    Create src/pages/api/exchange.ts:

    <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 }) => {
        <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>);
    };
    

    This grabs the code from the URL in the magic link sent, creates a server client, and calls the exchangeCodeForSession method with the code. It handles any error by redirecting to Astro’s built-in not-found page.

    Otherwise, it will redirect to the protected page as Supabase handles the session implementation details.

    Part 6: How to Test Your Application

    Start your development server: npm run dev

    Visit the provided localhost URL. You should see the sign-in page with the Turnstile widget:

    Sign-in page with Turnstile verification and email input field

    If you try to access the /protected 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 /protected page. This is what you should see:

    Text reads: "You are logged in!" with a field labeled "Your user Id" and a "Sign Out" button below.

    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.

    Notes and Additional Resources

    Useful Documentation

    • Supabase’s advanced guide to SSR

    • Supabase SSR package

    • Astro Cookies documentation

    • Supabase PKCE flow documentation

    • Astro Actions documentation

    • Get started with Turnstile

    Complete Code Repository

    The complete code for this project is available on GitHub:

    • Base authentication setup

    • With Cloudflare Turnstile

    Source: freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleHow to Start a Career in Technical Writing by Contributing to Open Source
    Next Article How to Assign Dataverse Security Roles at Scale

    Related Posts

    Development

    GenStudio for Performance Marketing: What’s New and What We’ve Learned

    September 19, 2025
    Development

    Agentic and Generative Commerce Can Elevate CX in B2B

    September 19, 2025
    Leave A Reply Cancel Reply

    For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.

    Continue Reading

    CVE-2025-4282 – SourceCodester Oretnom23 Stock Management System CSRF Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    Meet AlphaEarth Foundations: Google DeepMind’s So Called ‘ Virtual Satellite’ in AI-Driven Planetary Mapping

    Machine Learning

    Developer Spotlight: MisterPrada

    News & Updates

    Will you sync your Windows 10 PC data to the cloud for free access to security updates beyond 2025?

    News & Updates

    Highlights

    Critical Linux Privilege Escalation Vulnerabilities Let Attackers Gain Full Root Access

    June 18, 2025

    Critical Linux Privilege Escalation Vulnerabilities Let Attackers Gain Full Root Access

    Two critical, interconnected flaws, CVE-2025-6018 and CVE-2025-6019, enable unprivileged attackers to achieve root access on major Linux distributions.
    Affecting millions worldwide, these vulnerabilit …
    Read more

    Published Date:
    Jun 18, 2025 (4 hours, 1 minute ago)

    Vulnerabilities has been mentioned in this article.

    CVE-2025-1495 – IBM Business Automation Workflow Information Disclosure Vulnerability

    May 3, 2025

    Opsera Raises $20M to Drive AI-Powered DevOps Platform Innovation, Accelerating AI Agent Adoption and Developer Efficiency

    April 3, 2025

    An Introduction to PAPSS – Pan African Payment and Settlement System

    June 19, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.