Remix and Supabase Authentication
Tags: javascript, webdev, react, remix
Published: 2021-12-09 13:09:57

## How to secure a Remix and Supabase application using Row Level Security


> NOTE: This tutorial is deprecated in favor of the [official documentation](https://supabase.com/docs/guides/auth/auth-helpers/remix).


<br />

## Table of Contents
  * <a href="#tldr" target="_self">TL;DR Source and Demo</a>
  * <a href="#introduction" target="_self">Introduction</a>
  * <a href="#supabase-setup" target="_self">Setting up Supabase</a>
  * <a href="#server-side-utils" target="_self">Server-side utilities</a>
  * <a href="#client-side-utils" target="_self">Client-side utilities</a>
  * <a href="#sign-in" target="_self">Create sign-up and sign-in page</a>
  * <a href="#sign-out" target="_self">Create a sign-out action</a>
  * <a href="#tldr-examples" target="_self">TL;DR version of using the setup</a>
  * <a href="#fetch-all" target="_self">Fetch All example</a>
  * <a href="#get-delete-one" target="_self">Get one and Delete one example</a>
  * <a href="#create-one" target="_self">Create one example</a>
  * <a href="#update-one" target="_self">Update one example</a>
  * <a href="#conclusion" target="_self">Conclusion</a>

<br />


> ### TL;DR: Source and Demo <a name="tldr"></a>
> Here's a live [demo](https://playground-iirrt7a6j-codegino.vercel.app)
>
> Link to the [source code](https://github.com/codegino/playground/tree/part4/authentication)
>
> Link to [step by step commits](https://github.com/codegino/playground/commits/part4/authentication)

<br />

### Introduction <a name="introduction"></a>
This blog will focus on securing our Remix application with Supabase's [Row Level Security (RLS) feature](https://supabase.com/docs/guides/auth/row-level-security).
If you want to know the context of what application I'm talking about, you can refer to my [another blog](https://dev.to/codegino/remix-and-supabase-integration-cci).

<br />

### Setting up Supabase <a name="supabase-setup"></a>

> Instead of updating my database from the previous blog, I'm just going to re-create it.

#### Create a table to contain `user_id`

```sql
CREATE TABLE words (
  id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  name varchar NOT NULL,
  definitions varchar ARRAY NOT NULL,
  sentences varchar ARRAY NOT NULL,
  type varchar NOT NULL,
  user_id uuid NOT NULL
);
```
<br />

#### Add a foreign key in `user_id` pointing to `auth.users`

```sql
alter table words
add constraint words_users_fk
foreign key (user_id)
references auth.users (id);
```

<br />

#### Create Row Level Security Supabase Policies

```sql
CREATE POLICY "anon_select" ON public.words FOR SELECT USING (
  auth.role() = 'anon' or auth.role() = 'authenticated'
);

CREATE POLICY "auth_insert" ON public.words FOR INSERT WITH CHECK (
  auth.role() = 'authenticated'
);

CREATE POLICY "user_based__update" ON public.words FOR UPDATE USING (
  auth.uid() = user_id
);

CREATE POLICY "user_based_delete" ON public.words FOR DELETE USING (
  auth.uid() = user_id
);
```

<br />

### Implement server-side utilities to manage Supabase session <a name="server-side-utils"></a>

#### Create server instance of Supabase client

```ts
// app/utils/supabase.server.ts
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = process.env.SUPABASE_URL as string;
const supabaseKey = process.env.SUPABASE_ANON_KEY as string;

export const supabase = createClient(supabaseUrl, supabaseKey);
```

<br />

#### Use `createCookieSessionStorage` to help in managing our Supabase token

```ts
// app/utils/supabase.server.ts
// ...
import { createCookieSessionStorage } from "remix";

// ...

const { getSession, commitSession, destroySession } =
  createCookieSessionStorage({
    // a Cookie from `createCookie` or the CookieOptions to create one
    cookie: {
      name: "supabase-session",

      // all of these are optional
      expires: new Date(Date.now() + 3600),
      httpOnly: true,
      maxAge: 60,
      path: "/",
      sameSite: "lax",
      secrets: ["s3cret1"],
      secure: true,
    },
  });

export { getSession, commitSession, destroySession };
```

<br />

#### Create a utility to set the Supabase token from the Request

```ts
// app/utils/supabase.server.ts
// ...

export const setAuthToken = async (request: Request) => {
  let session = await getSession(request.headers.get("Cookie"));

  supabase.auth.setAuth(session.get("access_token"));

  return session;
};
```

<br />

## Setting up authentication in the Remix side 

### Create client-side utilities for managing Supabase session <a name="client-side-utils"></a>

#### Create Supabase Provider and a custom hook which returns the Supabase instance

```tsx
// app/utils/supabase-client.tsx
import { SupabaseClient } from "@supabase/supabase-js";
import React from "react";

export const SupabaseContext = React.createContext<SupabaseClient>(
  null as unknown as SupabaseClient
);

export const SupabaseProvider: React.FC<{ supabase: SupabaseClient }> = ({
  children,
  supabase,
}) => (
  <SupabaseContext.Provider value={supabase}>
    {children}
  </SupabaseContext.Provider>
);

export const useSupabase = () => React.useContext(SupabaseContext);
```

<br />

#### Pass Supabase environment variables to our client

```tsx
// app/root.tsx
export const loader = () => {
  return {
    supabaseKey: process.env.SUPABASE_ANON_KEY,
    supabaseUrl: process.env.SUPABASE_URL,
  };
};
```

<br />

#### Create a Supabase instance and pass it into the root level Supabase provider

```tsx
// app/root.tsx
import { createClient } from "@supabase/supabase-js";
import { SupabaseProvider } from "./utils/supabase-client";

// export const loader...

export default function App() {
  const loader = useLoaderData();

  const supabase = createClient(loader.supabaseUrl, loader.supabaseKey);

  return (
    <Document>
      <SupabaseProvider supabase={supabase}>
        <Layout>
          <Outlet />
        </Layout>
      </SupabaseProvider>
    </Document>
  );
}
```

<br />

### Create the `/auth` route <a name="sign-in"></a>

> Since I'm too lazy to implement a login page, I'll just use the UI provided by Supabase.

#### Install `@supabase/ui`

```sh
npm install @supabase/ui

yarn add @supabase/ui
```

<br />

#### Create the main auth component

> You can create your custom sign-up and sign-in form if you want. 

```tsx
// app/routes/auth.tsx
import React from "react";
import { Auth } from "@supabase/ui";
import { useSupabase } from "~/utils/supabase-client";

export default function AuthBasic() {
  const supabase = useSupabase();

  return (
    <Auth.UserContextProvider supabaseClient={supabase}>
      <Container> {/* TODO */}
        <Auth supabaseClient={supabase} />
      </Container>
    </Auth.UserContextProvider>
  );
}
```

<br />

#### Create the component to inform the server that we have a Supabase session

```tsx
// app/routes/auth.tsx
import React, { useEffect } from "react";
import { useSubmit } from "remix";

const Container: React.FC = ({ children }) => {
  const { user, session } = Auth.useUser();
  const submit = useSubmit();

  useEffect(() => {
    if (user) {
      const formData = new FormData();

      const accessToken = session?.access_token;

      // you can choose whatever conditions you want
      // as long as it checks if the user is signed in
      if (accessToken) {
        formData.append("access_token", accessToken);
        submit(formData, { method: "post", action: "/auth" });
      }
    }
  }, [user]);

  return <>{children}</>;
};

// ...
```

<br />

#### Create an action handler to process the Supabase token

```tsx
// app/routes/auth.tsx
import { Auth } from "@supabase/ui";
import { useSubmit, redirect } from "remix";
import type { ActionFunction } from "remix";
import React from "react";
import { useSupabase } from "~/utils/supabase-client";
import { commitSession, getSession } from "~/utils/supabase.server";

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();

  const session = await getSession(request.headers.get("Cookie"));

  session.set("access_token", formData.get("access_token"));

  return redirect("/words", {
    headers: {
      "Set-Cookie": await commitSession(session),
    },
  });
};

// ...
```

After logging in, the user will be redirected to the `/words` route.

<br />

> If you want to test without signing up, use the following credentials:
>
> email: dev.codegino@gmail.com
>
> password: testing

<br />

### Signing out <a name="sign-out"></a>

#### Create a logout button in the header

```tsx
// app/root.tsx
import { {/*...*/}, useSubmit } from "remix";
import { {/*...*/}, useSupabase } from "./utils/supabase-client";
import { Button } from "./components/basic/button";

function Layout({ children }: React.PropsWithChildren<{}>) {
  const submit = useSubmit();
  const supabase = useSupabase();

  const handleSignOut = () => {
    supabase.auth.signOut().then(() => {
      submit(null, { method: "post", action: "/signout" });
    });
  };

  return (
    <main>
      <header>
        {supabase.auth.session() && (
          <Button type="button" onClick={handleSignOut}>
            Sign out
          </Button>
        )}
      </header>
      {children}
    </main>
  );
}
```

<br />

#### Create an action handler

I don't want to pollute my other route, so I will create my signout action handler separately

```tsx
// app/routes/signout.tsx
import { destroySession, getSession } from "../utils/supabase.server";
import { redirect } from "remix";
import type { ActionFunction } from "remix";

export const action: ActionFunction = async ({ request }) => {
  let session = await getSession(request.headers.get("Cookie"));

  return redirect("/auth", {
    headers: {
      "Set-Cookie": await destroySession(session),
    },
  });
};

export const loader = () => {
  // Redirect to `/` if user tried to access `/signout`
  return redirect("/");
};
```

<br />


### TL;DR version of using our setup <a name="tldr-examples"></a>

#### Using in a `loader` or `action`
```tsx
export const action = async ({ request, params }) => {
  // Just set the token to any part you want to have access to.
  // I haven't tried making a global handler for this,
  // but I prefer to be explicit about setting this.
  await setAuthToken(request);

  await supabase.from("words").update(/*...*/);
  // ...
};
```

<br />

#### Conditional rendering based on auth state

```tsx
export default function Index() {
  const supabase = useSupabase();

  return supabase.auth.user()
    ? <div>Hello world</div>
    : <div>Please sign in</div>;
}
```

<br />

> NOTE: Conditional server-side rendering might cause hydration warning,
>
> I'll fix this in another blog post.

<br />

## Using in CRUD Operations
The examples below are a longer version of using our setup for CRUD operations.

### Fetching All operation <a name="fetch-all"></a>

```tsx
// app/routes/words
import { Form, useTransition } from "remix";
import type { LoaderFunction } from "remix";
import { useLoaderData, Link, Outlet } from "remix";
import { Button } from "~/components/basic/button";
import { supabase } from "~/utils/supabase.server";
import type { Word } from "~/models/word";
import { useSupabase } from "~/utils/supabase-client";

export const loader: LoaderFunction = async () => {
  // No need to add auth here, because GET /words is public
  const { data: words } = await supabase
    .from<Word>("words")
    .select("id,name,type");

  // We can pick and choose what we want to display
  // This can solve the issue of over-fetching or under-fetching
  return words;
};

export default function Index() {
  const words = useLoaderData<Word[]>();
  const transition = useTransition();
  const supabase = useSupabase();

  return (
    <main className="p-2">
      <h1 className="text-3xl text-center mb-3">English words I learned</h1>
      <div className="text-center mb-2">Route State: {transition.state}</div>
      <div className="grid grid-cols-1 md:grid-cols-2 ">
        <div className="flex flex-col items-center">
          <h2 className="text-2xl pb-2">Words</h2>
          <ul>
            {words.map((word) => (
              <li key={word.id}>
                <Link to={`/words/${word.id}`}>
                  {word.name} | {word.type}
                </Link>
              </li>
            ))}
          </ul>
          {/* Adding conditional rendering might cause a warning,
          We'll deal with it later */}
          {supabase.auth.user() ? (
            <Form method="get" action={"/words/add"} className="pt-2">
              <Button
                type="submit"
                className="hover:bg-primary-100 dark:hover:bg-primary-900"
              >
                Add new word
              </Button>
            </Form>
          ) : (
            <Form method="get" action={`/auth`} className="flex">
              <Button type="submit" color="primary" className="w-full">
                Sign-in to make changes
              </Button>
            </Form>
          )}
        </div>
        <Outlet />
      </div>
    </main>
  );
}
```

<br />

### Retrieve one and Delete one operation <a name="get-delete-one"></a>

```tsx
// app/routes/words/$id
import { Form, useLoaderData, redirect, useTransition } from "remix";
import type { LoaderFunction, ActionFunction } from "remix";
import type { Word } from "~/models/word";
import { Input } from "~/components/basic/input";
import { Button } from "~/components/basic/button";
import { setAuthToken, supabase } from "~/utils/supabase.server";
import { useSupabase } from "~/utils/supabase-client";

// Here's how to delete one entry
export const action: ActionFunction = async ({ request, params }) => {
  const formData = await request.formData();

  // Auth Related Code
  await setAuthToken(request);

  if (formData.get("_method") === "delete") {
    await supabase
      .from<Word>("words")
      .delete()
      .eq("id", params.id as string);

    return redirect("/words");
  }
};

// Here's the how to fetch one entry
export const loader: LoaderFunction = async ({ params }) => {
  // No need to add auth here, because GET /words is public
  const { data } = await supabase
    .from<Word>("words")
    .select("*")
    .eq("id", params.id as string)
    .single();

  return data;
};

export default function Word() {
  const word = useLoaderData<Word>();
  const supabase = useSupabase();
  let transition = useTransition();

  return (
    <div>
      <h3>
        {word.name} | {word.type}
      </h3>
      <div>Form State: {transition.state}</div>
      {word.definitions.map((definition, i) => (
        <p key={i}>
          <i>{definition}</i>
        </p>
      ))}
      {word.sentences.map((sentence, i) => (
        <p key={i}>{sentence}</p>
      ))}

      {/* Adding conditional rendering might cause a warning,
      We'll deal with it later */}
      {supabase.auth.user() && (
        <>
          <Form method="post">
            <Input type="hidden" name="_method" value="delete" />
            <Button type="submit" className="w-full">
              Delete
            </Button>
          </Form>
          <Form method="get" action={`/words/edit/${word.id}`} className="flex">
            <Button type="submit" color="primary" className="w-full">
              Edit
            </Button>
          </Form>
        </>
      )}
    </div>
  );
}
```

<br />

### Create operation <a name="create-one"></a>

```tsx
// app/routes/words/add
import { redirect } from "remix";
import type { ActionFunction } from "remix";
import { setAuthToken, supabase } from "~/utils/supabase.server";
import { WordForm } from "~/components/word-form";

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();

  // Auth Related Code
  const session = await setAuthToken(request);

  const newWord = {
    name: formData.get("name"),
    type: formData.get("type"),
    sentences: formData.getAll("sentence"),
    definitions: formData.getAll("definition"),
    user_id: session.get("uuid"),
  };

  const { data, error } = await supabase
    .from("words")
    .insert([newWord])
    .single();

  if (error) {
    return redirect(`/words`);
  }

  return redirect(`/words/${data?.id}`);
};

export default function AddWord() {
  return <WordForm />;
}
```

<br />

### Update operation <a name="update-one"></a>

```tsx
// app/routes/words/edit/$id
import { useLoaderData, redirect } from "remix";
import type { LoaderFunction, ActionFunction } from "remix";
import { WordForm } from "~/components/word-form";
import type { Word } from "~/models/word";
import { setAuthToken, supabase } from "~/utils/supabase.server";

export const action: ActionFunction = async ({ request, params }) => {
  const formData = await request.formData();
  const id = params.id as string;

  const updates = {
    type: formData.get("type"),
    sentences: formData.getAll("sentence"),
    definitions: formData.getAll("definition"),
  };

  // Auth Related Code
  await setAuthToken(request);

  await supabase.from("words").update(updates).eq("id", id);

  return redirect(`/words/${id}`);
};

export const loader: LoaderFunction = async ({ params }) => {
  const { data } = await supabase
    .from<Word>("words")
    .select("*")
    .eq("id", params.id as string)
    .single();

  return data;
};

export default function EditWord() {
  const data = useLoaderData<Word>();

  return <WordForm word={data} />;
}
```

<br />

## Conclusion <a name="conclusion"></a>
We can still use Supabase only on the client-side as we use it on a typical React application. However, putting the data fetching on the server-side will allow us to benefit from a typical [SSR application](https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8).