# Use Supabase Auth with Astro

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

1. **Create a new Supabase project**

Head over to [database.new](https://database.new) and create a new Supabase 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](/dashboard/project/_/sql/new).

```sql name=SQL_EDITOR
select * from auth.users;
```

2. **Create an Astro app**

Create a new Astro app using the `npm create` command.

UI components built on shadcn/ui that connect to Supabase via a single command.

Explore Components

```bash name=Terminal
npm create astro@latest my-app
cd my-app
```

3. **Install Supabase 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.

```bash name=Terminal
npm install @supabase/supabase-js @supabase/ssr @astrojs/node
```

4. **Configure Astro for SSR**

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

```js name=astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";

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

5. **Declare Supabase Environment Variables**

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

```text name=.env.local
PUBLIC_SUPABASE_URL=your-project-url
PUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_key
```

You can also get the Project URL and key from [the project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).

### Get API details

Now that you've created some database tables, you are ready to insert data using the auto-generated API.

To do this, you need to get the Project URL and key from [the project **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}).

[Read the API keys docs](/docs/guides/getting-started/api-keys) for a full explanation of all key types and their uses.

Supabase is changing the way keys work to improve project security and developer experience. You can [read the full announcement](https://github.com/orgs/supabase/discussions/29260), but in the transition period, you can use both the current `anon` and `service_role` keys and the new publishable key with the form `sb_publishable_xxx` which will replace the older keys.

**The legacy keys will be deprecated shortly, so we strongly encourage switching to and using the new publishable and secret API keys**.

In most cases, you can get the correct key from [the Project's **Connect** dialog](/dashboard/project/\_?showConnect=true&connectTab={{ .tab }}&framework={{ .framework }}), but if you want a specific key, you can find all keys in [the API Keys section of a Project's Settings page](/dashboard/project/_/settings/api-keys/):

**For new keys**, open the **API Keys** tab, if you don't have a publishable key already, click **Create new API Keys**, and copy the value from the **Publishable key** section.

6. **Create a Supabase client helper**

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

```ts name=src/lib/supabase.ts
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)
          );
        },
      },
    }
  );
}
```

7. **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:

```ts name=src/actions/index.ts
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",
        };
      }
    },
  }),
};
```

8. **Customize email template**

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

In your [Supabase project dashboard](/dashboard/project/_/auth/templates):
- 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](/dashboard/project/_/auth/url-configuration) to `http://localhost:4321`

```html name=Email\ Template
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email
```

9. **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 Supabase:

```astro name=src/pages/auth/callback.astro
---
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>
```

10. **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:

```astro name=src/pages/auth/signup.astro
---
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>

    <form id="signup-form">

        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          name="email"
          placeholder="your@email.com"
          required
        />

        <label for="password">Password</label>
        <input
          id="password"
          type="password"
          name="password"
          placeholder="At least 6 characters"
          required
        />

      <button type="submit" id="signup-btn">Sign Up</button>
    </form>
    <p>
      Already have an account? Sign in
    </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>
```

11. **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:

```astro name=src/pages/auth/signin.astro
---
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>

    <form id="signin-form">

        <label for="email">Email</label>
        <input
          id="email"
          type="email"
          name="email"
          placeholder="your@email.com"
          required
        />

        <label for="password">Password</label>
        <input
          id="password"
          type="password"
          name="password"
          placeholder="Your password"
          required
        />

      <button type="submit" id="signin-btn">Sign In</button>
    </form>
    <p>
      Don't have an account? Sign up
    </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>
```

12. **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:

```astro name=src/pages/dashboard.astro
---
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>
```

13. **Start the app**

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

```bash name=Terminal
npm run dev
```

## Learn more

- [Supabase Auth docs](/docs/guides/auth#authentication) for more Supabase authentication methods