Guide

Use Zuvo Auth with Astro

Learn how to configure Zuvo Auth for Astro with server-side rendering.

Quickstart

Create a new Zuvo project

Head over to database.new and create a new Zuvo project.

Your new database has a table for storing your users. You can see that this table is currently empty by running some SQL in the SQL Editor.

      select * from auth.users;

Create an Astro app

Create a new Astro app using the npm create command.

      npm create astro@latest my-app
      cd my-app

Install Zuvo libraries and Node adapter

Install the @supabase/supabase-js client library, @supabase/ssr for server-side auth, and the @astrojs/node adapter to enable server-side rendering.

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

Configure Astro for SSR

Update your astro.config.mjs to enable server-side rendering with the Node adapter.

      import { defineConfig } from "astro/config";
      import node from "@astrojs/node";

      export default defineConfig({
        output: "server",
        adapter: node({
          mode: "standalone",
        }),
      });

Declare Zuvo Environment Variables

Create a .env.local file and populate with your Zuvo connection variables:

      PUBLIC_SUPABASE_URL=your-project-url
      PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_key

Get API details

To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.

Create a Zuvo client helper

Create a utility file to initialize the Zuvo client with SSR support:

      import { createServerClient, parseCookieHeader } from "@supabase/ssr";
      import type { AstroCookies } from "astro";

      const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL
      const supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY

      export function createClient({
        request,
        cookies,
      }: {
        request: Request;
        cookies: AstroCookies;
      }) {
        return createServerClient(
          supabaseUrl,
          supabasePublishableKey,
          {
            cookies: {
              getAll() {
                return parseCookieHeader(
                  request.headers.get("Cookie") ?? ""
                );
              },
              setAll(cookiesToSet) {
                cookiesToSet.forEach(({ name, value, options }) =>
                  cookies.set(name, value, options)
                );
              },
            },
          }
        );
      }

Create authentication actions

Create a new file at src/actions/index.ts to define server-side authentication actions for signing up, signing in, and signing out:

      import { defineAction } from "astro:actions";
      import { z } from "astro/zod";
      import { createClient } from "../lib/supabase";

      export const server = {
        signUp: defineAction({
          accept: "form",
          input: z.object({
            email: z.string().email(),
            password: z.string().min(6),
          }),
          handler: async (input, context) => {
            try {
              const supabase = createClient({
                request: context.request,
                cookies: context.cookies,
              });

              const { error } = await supabase.auth.signUp({
                email: input.email,
                password: input.password,
                options: {
                  emailRedirectTo: "http://localhost:4321/auth/callback",
                },
              });

              if (error) {
                return {
                  success: false,
                  message: error.message,
                };
              }

              return {
                success: true,
                message: "Check your email to confirm your account",
              };
            } catch (err) {
              return {
                success: false,
                message: "Unexpected error",
              };
            }
          },
        }),
        signIn: defineAction({
          accept: "form",
          input: z.object({
            email: z.string().email(),
            password: z.string(),
          }),
          handler: async (input, context) => {
            try {
              const supabase = createClient({
                request: context.request,
                cookies: context.cookies,
              });

              const { error } = await supabase.auth.signInWithPassword({
                email: input.email,
                password: input.password,
              });

              if (error) {
                return {
                  success: false,
                  message: error.message,
                };
              }

              return {
                success: true,
                message: "Signed in successfully",
              };
            } catch (err) {
              return {
                success: false,
                message: "Unexpected error",
              };
            }
          },
        }),
        signOut: defineAction({
          handler: async (_, context) => {
            try {
              const supabase = createClient({
                request: context.request,
                cookies: context.cookies,
              });

              await supabase.auth.signOut();

              return {
                success: true,
              };
            } catch (err) {
              return {
                success: false,
                message: "Failed to sign out",
              };
            }
          },
        }),
      };

Customize email template

Before users can confirm their email, update the Zuvo email template to send the token hash to your callback URL.

In your Zuvo project dashboard:

  • Go to Auth > Email Templates
  • Select the Confirm signup template
  • Change ConfirmationURL to SiteURL/auth/callback?token_hash=TokenHash&type=email.
  • Change your Site URL to http://localhost:4321
      {{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email

Create an auth callback page

Create a new file at src/pages/auth/callback.astro to handle the email confirmation callback. Extract the token from the URL and verify it with Zuvo:

      ---
      import { createClient } from "../../lib/supabase";
      import type { EmailOtpType } from "@supabase/supabase-js";

      const supabase = createClient({
        request: Astro.request,
        cookies: Astro.cookies,
      });

      const requestUrl = new URL(Astro.request.url);
      const token_hash = requestUrl.searchParams.get('token_hash');
      const type = requestUrl.searchParams.get('type') as EmailOtpType | null;

      if (token_hash && type) {
        const { error } = await supabase.auth.verifyOtp({
          token_hash,
          type,
        });

        if (!error) {
          return Astro.redirect("/dashboard");
        }
      }

      return Astro.redirect("/auth/signin");
      ---

      <html>
        <head>
          <title>Email Confirmation</title>
        </head>
        <body>
          <p>Confirming your email...</p>
        </body>
      </html>

Create a sign-up page

Create a new file at src/pages/auth/signup.astro with a sign-up form. Use a client-side event listener to handle form submission:

      ---
      import { createClient } from "../../lib/supabase";

      const supabase = createClient({
        request: Astro.request,
        cookies: Astro.cookies,
      });

      const { data } = await supabase.auth.getUser();

      if (data?.user) {
        return Astro.redirect("/dashboard");
      }
      ---

      <html>
        <head>
          <title>Sign Up</title>
        </head>
        <body>
          <h1>Sign Up</h1>

          <div id="message"></div>

          <form id="signup-form">
            <div>
              <label for="email">Email</label>
              <input
                id="email"
                type="email"
                name="email"
                placeholder="your@email.com"
                required
              />
            </div>
            <div>
              <label for="password">Password</label>
              <input
                id="password"
                type="password"
                name="password"
                placeholder="At least 6 characters"
                required
              />
            </div>
            <button type="submit" id="signup-btn">Sign Up</button>
          </form>
          <p>
            Already have an account? <a href="/auth/signin">Sign in</a>
          </p>
        </body>
      </html>

      <script>
        import { actions } from "astro:actions";

        const form = document.querySelector("#signup-form") as HTMLFormElement;
        const btn = document.getElementById("signup-btn") as HTMLButtonElement;
        const messageEl = document.getElementById("message") as HTMLDivElement;

        form?.addEventListener("submit", async (e) => {
          e.preventDefault();
          btn.disabled = true;
          btn.textContent = "Signing up...";
          messageEl.textContent = "";

          try {
            const formData = new FormData(form);
            const result = await actions.signUp(formData);

            if (!result.data?.success) {
              btn.disabled = false;
              btn.textContent = "Sign Up";
              messageEl.textContent = result.data?.message || "Sign up failed";
              messageEl.style.color = "red";
              return;
            }

            messageEl.textContent = result.data.message;
            messageEl.style.color = "green";
            btn.textContent = "Sign Up";
          } catch (error) {
            btn.disabled = false;
            btn.textContent = "Sign Up";
            messageEl.textContent = "An error occurred. Please try again.";
            messageEl.style.color = "red";
            console.error(error);
          }
        });
      </script>

Create a sign-in page

Create a new file at src/pages/auth/signin.astro with a sign-in form. Use a client-side event listener to handle form submission:

      ---
      import { createClient } from "../../lib/supabase";

      const supabase = createClient({
        request: Astro.request,
        cookies: Astro.cookies,
      });

      const { data } = await supabase.auth.getUser();

      if (data?.user) {
        return Astro.redirect("/dashboard");
      }
      ---

      <html>
        <head>
          <title>Sign In</title>
        </head>
        <body>
          <h1>Sign In</h1>

          <div id="message"></div>

          <form id="signin-form">
            <div>
              <label for="email">Email</label>
              <input
                id="email"
                type="email"
                name="email"
                placeholder="your@email.com"
                required
              />
            </div>
            <div>
              <label for="password">Password</label>
              <input
                id="password"
                type="password"
                name="password"
                placeholder="Your password"
                required
              />
            </div>
            <button type="submit" id="signin-btn">Sign In</button>
          </form>
          <p>
            Don't have an account? <a href="/auth/signup">Sign up</a>
          </p>
        </body>
      </html>

      <script>
        import { actions } from "astro:actions";

        const form = document.querySelector("#signin-form") as HTMLFormElement;
        const btn = document.getElementById("signin-btn") as HTMLButtonElement;
        const messageEl = document.getElementById("message") as HTMLDivElement;

        form?.addEventListener("submit", async (e) => {
          e.preventDefault();
          btn.disabled = true;
          btn.textContent = "Signing in...";
          messageEl.textContent = "";

          try {
            const formData = new FormData(form);
            const result = await actions.signIn(formData);

            if (!result.data?.success) {
              btn.disabled = false;
              btn.textContent = "Sign In";
              messageEl.textContent = result.data?.message || "Sign in failed";
              messageEl.style.color = "red";
              return;
            }

            // Redirect to dashboard on successful sign in
            window.location.href = "/dashboard";
          } catch (error) {
            btn.disabled = false;
            btn.textContent = "Sign In";
            messageEl.textContent = "An error occurred. Please try again.";
            messageEl.style.color = "red";
            console.error(error);
          }
        });
      </script>

Create a dashboard page

Create a new file at src/pages/dashboard.astro to display the authenticated user's information. Use a client-side event listener for the sign-out button:

      ---
      import { createClient } from "../lib/supabase";

      const supabase = createClient({
        request: Astro.request,
        cookies: Astro.cookies,
      });

      const { data } = await supabase.auth.getUser();
      const user = data?.user;

      if (!user) {
        return Astro.redirect("/auth/signin");
      }
      ---

      <html>
        <head>
          <title>Dashboard</title>
        </head>
        <body>
          <h1>Welcome!</h1>
          <p>Email: {user.email}</p>
          <p>User ID: {user.id}</p>

          <button id="signout-btn">Sign Out</button>
        </body>
      </html>

      <script>
        import { actions } from "astro:actions";

        const btn = document.getElementById("signout-btn") as HTMLButtonElement;

        btn?.addEventListener("click", async (e) => {
          e.preventDefault();
          btn.disabled = true;
          btn.textContent = "Signing out...";

          try {
            const result = await actions.signOut();

            if (!result.data?.success) {
              btn.disabled = false;
              btn.textContent = "Sign Out";
              alert("Failed to sign out");
              return;
            }

            // Redirect to signin page
            window.location.href = "/auth/signin";
          } catch (error) {
            btn.disabled = false;
            btn.textContent = "Sign Out";
            console.error(error);
          }
        });
      </script>

Start the app

Start the development server, then navigate to http://localhost:4321/auth/signup to test the authentication.

      npm run dev

Learn more