npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

remix-auth-supabase

v4.0.2

Published

> Strategy for using supabase with Remix Auth

Readme

Remix Auth - Supabase Strategy

Strategy for using supabase with Remix Auth

This strategy aims to provide an easy way to start using supabase for authentication in Remix.run apps! It uses remix-auth as its base bootstrapped from remix-auth-template) (thanks @sergiodxa 🚀).

📖 Examples

NOTICE 🚧

This library is maintained by people who do not make a living creating and maintining open source libraries, it's just a hobby and life takes priority over hobbies.

Having that said: Remix-auth-strategy was not designed with the intention to handle everything that we currently handle (refreshing tokens for example). This unfortunately could lead to scenarios where some of the features may not work as expected (beware).

Having all of this said, we're happy to keep this library alive for anyone who wants to (keep) using it.

💜

Our official recommendation for production grade remix applications with supabase is to use the supa fly stack by RPHLMR 😎

Supported runtimes

| Runtime | Has Support | | ---------- | ----------- | | Node.js | ✅ | | Cloudflare | ✅ |

  • Cloudflare works but it does require to supply fetch to the supabase-client as an option

Introduction

The way remix-auth (and it's templates) are designed are not a direct fit for what I had in mind for a seamless authentication strategy with Supabase. After some back and forth between the community and playing around with various setups in vitest ⚡ we decided to create a strategy that supports the following:

  • Multiple authentication strategies thanks to remix-auth and the verify method (more on this later)
  • User object, access_tokens and refresh_tokens are stored in a cookie
  • checkSession method to protect routes (like authenticator.isAuthenticated) and handle refreshing of expired tokens

How to use

Install the package (and remix-auth)

  • yarn add remix-auth remix-auth-supabase
  • pnpm install remix-auth remix-auth-supabase
  • npm install remix-auth remix-auth-supabase

Breaking change v2 to v3

To allow for more freedom and support some of the different authentication types the verify no longer just sends the form, but it now sends the entire request. See Setup authenticator & strategy

Setup sessionStorage, strategy & authenticator

// app/auth.server.ts
import { createCookieSessionStorage } from '@remix-run/node';
import { Authenticator, AuthorizationError } from 'remix-auth';
import { SupabaseStrategy } from 'remix-auth-supabase-strategy';
import { Session, supabaseClient } from '~/supabase';

export const sessionStorage = createCookieSessionStorage({
  cookie: {
    name: 'sb',
    httpOnly: true,
    path: '/',
    sameSite: 'lax',
    secrets: ['s3cr3t'], // This should be an env variable
    secure: process.env.NODE_ENV === 'production'
  }
});

export const supabaseStrategy = new SupabaseStrategy(
  {
    supabaseClient,
    sessionStorage,
    sessionKey: 'sb:session', // if not set, default is sb:session
    sessionErrorKey: 'sb:error' // if not set, default is sb:error
  },
  // simple verify example for email/password auth
  async ({ req, supabaseClient }) => {
    const form = await req.formData();
    const email = form?.get('email');
    const password = form?.get('password');

    if (!email) throw new AuthorizationError('Email is required');
    if (typeof email !== 'string') throw new AuthorizationError('Email must be a string');

    if (!password) throw new AuthorizationError('Password is required');
    if (typeof password !== 'string') throw new AuthorizationError('Password must be a string');

    return supabaseClient.auth.api.signInWithEmail(email, password).then(({ data, error }): Session => {
      if (error || !data) {
        throw new AuthorizationError(error?.message ?? 'No user session found');
      }

      return data;
    });
  }
);

export const authenticator =
  new Authenticator() <
  Session >
  (sessionStorage,
  {
    sessionKey: supabaseStrategy.sessionKey, // keep in sync
    sessionErrorKey: supabaseStrategy.sessionErrorKey // keep in sync
  });

authenticator.use(supabaseStrategy);

Using the authenticator & strategy 🚀

checkSession works like authenticator.isAuthenticated but handles token refresh

// app/routes/login.ts
export const loader: LoaderFunction = async ({ request }) =>
  supabaseStrategy.checkSession(request, {
    successRedirect: '/private'
  });

export const action: ActionFunction = async ({ request }) =>
  authenticator.authenticate('sb', request, {
    successRedirect: '/private',
    failureRedirect: '/login'
  });

export default function LoginPage() {
  return (
    <Form method="post">
      <input type="email" name="email" />
      <input type="password" name="password" />
      <button>Sign In</button>
    </Form>
  );
}
// app/routes/private.ts
export const loader: LoaderFunction = async ({ request }) => {
  // If token refresh and successRedirect not set, reload the current route
  const session = await supabaseStrategy.checkSession(request);

  if (!session) {
    // If the user is not authenticated, you can do something or nothing
    // ⚠️ If you do nothing, /profile page is display
  }
};

// Handle logout action
export const action: ActionFunction = async ({ request }) => {
  await authenticator.logout(request, { redirectTo: '/login' });
};
Refresh token or redirect
// If token is refreshing and successRedirect not set, it reloads the current route
await supabaseStrategy.checkSession(request, {
  failureRedirect: '/login'
});
Redirect if authenticated
// If the user is authenticated, redirect to /private
await supabaseStrategy.checkSession(request, {
  successRedirect: '/private'
});
Get session or null : decide what to do
// Get the session or null, and do different things in your loader/action based on
// the result
const session = await supabaseStrategy.checkSession(request);
if (session) {
  // Here the user is authenticated
} else {
  // Here the user is not authenticated
}

Tips

Prevent infinite loop 😱

// app/routes/login.ts
export const loader: LoaderFunction = async ({ request }) => {
  // Beware, never set failureRedirect equals to the current route
  const session = supabaseStrategy.checkSession(request, {
    successRedirect: '/private',
    failureRedirect: '/login' // ❌ DONT'T : infinite loop
  });

  // In this example, session is always null otherwise it would have been redirected
};

Redirect to

Example

With Remix.run it's easy to add super UX

// app/routes/private.profile.ts
export const loader: LoaderFunction = async ({ request }) =>
  // If checkSession fails, redirect to login and go back here when authenticated
  supabaseStrategy.checkSession(request, {
    failureRedirect: '/login?redirectTo=/private/profile'
  });
// app/routes/private.ts
export const loader: LoaderFunction = async ({ request }) =>
  // If checkSession fails, redirect to login and go back here when authenticated
  supabaseStrategy.checkSession(request, {
    failureRedirect: '/login'
  });
// app/routes/login.ts
export const loader = async ({ request }) => {
  const redirectTo = new URL(request.url).searchParams.get('redirectTo') ?? '/profile';

  return supabaseStrategy.checkSession(request, {
    successRedirect: redirectTo
  });
};

export const action: ActionFunction = async ({ request }) => {
  // Always clone request when access formData() in action/loader with authenticator
  // 💡 request.formData() can't be called twice
  const data = await request.clone().formData();
  // If authenticate success, redirectTo what found in searchParams
  // Or where you want
  const redirectTo = data.get('redirectTo') ?? '/profile';

  return authenticator.authenticate('sb', request, {
    successRedirect: redirectTo,
    failureRedirect: '/login'
  });
};

export default function LoginPage() {
  const [searchParams] = useSearchParams();

  return (
    <Form method="post">
      <input name="redirectTo" value={searchParams.get('redirectTo') ?? undefined} hidden readOnly />
      <input type="email" name="email" />
      <input type="password" name="password" />
      <button>Sign In</button>
    </Form>
  );
}