--- title: 'Build a User Management App with RedwoodJS' description: 'Learn how to use Supabase in your RedwoodJS App.' --- <$Partial path="quickstart_intro.mdx" /> ![Supabase User Management example](/docs/img/user-management-demo.png) If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart). ## About RedwoodJS A Redwood application is split into two parts: a frontend and a backend. This is represented as two node projects within a single monorepo. The frontend project is called **`web`** and the backend project is called **`api`**. For clarity, we will refer to these in prose as **"sides,"** that is, the `web side` and the `api side`. They are separate projects because code on the `web side` will end up running in the user's browser while code on the `api side` will run on a server somewhere. Important: When this guide refers to "API," that means the Supabase API and when it refers to `api side`, that means the RedwoodJS `api side`. The **`api side`** is an implementation of a GraphQL API. The business logic is organized into "services" that represent their own internal API and can be called both from external GraphQL requests and other internal services. The **`web side`** is built with React. Redwood's router makes it simple to map URL paths to React "Page" components (and automatically code-split your app on each route). Pages may contain a "Layout" component to wrap content. They also contain "Cells" and regular React components. Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data. For the sake of consistency with the other framework tutorials, we'll build this app a little differently than normal. We **_won't use_** Prisma to connect to the Supabase Postgres database or [Prisma migrations](https://redwoodjs.com/docs/cli-commands#prisma-migrate) as one typically might in a Redwood app. Instead, we'll rely on the Supabase client to do some of the work on the **`web`** side and use the client again on the **`api`** side to do data fetching as well. That means you will want to refrain from running any `yarn rw prisma migrate` commands and also double check your build commands on deployment to ensure Prisma won't reset your database. Prisma currently doesn't support cross-schema foreign keys, so introspecting the schema fails due to how your Supabase `public` schema references the `auth.users`. <$Partial path="project_setup.mdx" /> ## Building the app Let's start building the RedwoodJS app from scratch. RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`. Make sure you have installed yarn since RedwoodJS relies on it to [manage its packages in workspaces](https://classic.yarnpkg.com/lang/en/docs/workspaces/) for its `web` and `api` "sides." ### Initialize a RedwoodJS app We can use [Create Redwood App](https://redwoodjs.com/docs/quick-start) command to initialize an app called `supabase-redwoodjs`: ```bash yarn create redwood-app supabase-redwoodjs cd supabase-redwoodjs ``` While the app is installing, you should see: ```bash ✔ Creating Redwood app ✔ Checking node and yarn compatibility ✔ Creating directory 'supabase-redwoodjs' ✔ Installing packages ✔ Running 'yarn install'... (This could take a while) ✔ Convert TypeScript files to JavaScript ✔ Generating types Thanks for trying out Redwood! ``` Then let's install the only additional dependency [supabase-js](https://github.com/supabase/supabase-js) by running the `setup auth` command: ```bash yarn redwood setup auth supabase ``` When prompted: > Overwrite existing /api/src/lib/auth.[jt]s? Say, **yes** and it will setup the Supabase client in your app and also provide hooks used with Supabase authentication. ```bash ✔ Generating auth lib... ✔ Successfully wrote file `./api/src/lib/auth.js` ✔ Adding auth config to web... ✔ Adding auth config to GraphQL API... ✔ Adding required web packages... ✔ Installing packages... ✔ One more thing... You will need to add your Supabase URL (SUPABASE_URL), public API KEY, and JWT SECRET (SUPABASE_KEY, and SUPABASE_JWT_SECRET) to your .env file. ``` Next, we want to save the environment variables in a `.env`. We need the `API URL` as well as the `anon` and `jwt_secret` keys that you copied [earlier](#get-the-api-keys). <$CodeTabs> ```bash name=.env SUPABASE_URL=YOUR_SUPABASE_URL SUPABASE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY SUPABASE_JWT_SECRET=YOUR_SUPABASE_JWT_SECRET ``` And finally, you will also need to save **just** the `web side` environment variables to the `redwood.toml`. <$CodeTabs> ```bash name=redwood.toml [web] title = "Supabase Redwood Tutorial" port = 8910 apiProxyPath = "/.redwood/functions" includeEnvironmentVariables = ["SUPABASE_URL", "SUPABASE_KEY"] [api] port = 8911 [browser] open = true ``` These variables will be exposed on the browser, and that's completely fine. They allow your web app to initialize the Supabase client with your public anon key since we have [Row Level Security](/docs/guides/auth#row-level-security) enabled on our Database. You'll see these being used to configure your Supabase client in `web/src/App.js`: <$CodeTabs> ```js name=web/src/App.js // ... Redwood imports import { AuthProvider } from '@redwoodjs/auth' import { createClient } from '@supabase/supabase-js' // ... const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_KEY) const App = () => ( ) export default App ``` ### App styling (optional) An optional step is to update the CSS file `web/src/index.css` to make the app look nice. You can find the full contents of this file [here](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/react-user-management/src/index.css). ### Start RedwoodJS and your first page Let's test our setup at the moment by starting up the app: ```bash yarn rw dev ``` `rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands. You should see a "Welcome to RedwoodJS" page and a message about not having any pages yet. So, let's create a "home" page: ```bash yarn rw generate page home / ✔ Generating page files... ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.stories.js` ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.test.js` ✔ Successfully wrote file `./web/src/pages/HomePage/HomePage.js` ✔ Updating routes file... ✔ Generating types ... ``` The `/` is important here as it creates a root level route. You can stop the `dev` server if you want; to see your changes, just be sure to run `yarn rw dev` again. You should see the `Home` page route in `web/src/Routes.js`: <$CodeTabs> ```bash name=web/src/Routes.js import { Router, Route } from '@redwoodjs/router' const Routes = () => { return ( ) } export default Routes ``` ### Set up a login component Let's set up a Redwood component to manage logins and sign ups. We'll use Magic Links, so users can sign in with their email without using passwords. ```bash yarn rw g component auth ✔ Generating component files... ✔ Successfully wrote file `./web/src/components/Auth/Auth.test.js` ✔ Successfully wrote file `./web/src/components/Auth/Auth.stories.js` ✔ Successfully wrote file `./web/src/components/Auth/Auth.js` ``` Now, update the `Auth.js` component to contain: <$CodeTabs> ```jsx name=/web/src/components/Auth/Auth.js import { useState } from 'react' import { useAuth } from '@redwoodjs/auth' const Auth = () => { const { logIn } = useAuth() const [loading, setLoading] = useState(false) const [email, setEmail] = useState('') const handleLogin = async (email) => { try { setLoading(true) const { error } = await logIn({ email }) if (error) throw error alert('Check your email for the login link!') } catch (error) { alert(error.error_description || error.message) } finally { setLoading(false) } } return (

Supabase + RedwoodJS

Sign in via magic link with your email below

setEmail(e.target.value)} />
) } export default Auth ``` ### Set up an account component After a user is signed in we can allow them to edit their profile details and manage their account. Let's create a new component for that called `Account.js`. ```bash yarn rw g component account ✔ Generating component files... ✔ Successfully wrote file `./web/src/components/Account/Account.test.js` ✔ Successfully wrote file `./web/src/components/Account/Account.stories.js` ✔ Successfully wrote file `./web/src/components/Account/Account.js` ``` And then update the file to contain: <$CodeTabs> ```jsx name=web/src/components/Account/Account.js import { useState, useEffect } from 'react' import { useAuth } from '@redwoodjs/auth' const Account = () => { const { client: supabase, currentUser, logOut } = useAuth() const [loading, setLoading] = useState(true) const [username, setUsername] = useState(null) const [website, setWebsite] = useState(null) const [avatar_url, setAvatarUrl] = useState(null) useEffect(() => { getProfile() }, [supabase.auth.session]) async function getProfile() { try { setLoading(true) const user = supabase.auth.user() const { data, error, status } = await supabase .from('profiles') .select(`username, website, avatar_url`) .eq('id', user.id) .single() if (error && status !== 406) { throw error } if (data) { setUsername(data.username) setWebsite(data.website) setAvatarUrl(data.avatar_url) } } catch (error) { alert(error.message) } finally { setLoading(false) } } async function updateProfile({ username, website, avatar_url }) { try { setLoading(true) const user = supabase.auth.user() const updates = { id: user.id, username, website, avatar_url, updated_at: new Date(), } const { error } = await supabase.from('profiles').upsert(updates, { returning: 'minimal', // Don't return the value after inserting }) if (error) { throw error } alert('Updated profile!') } catch (error) { alert(error.message) } finally { setLoading(false) } } return (

Supabase + RedwoodJS

Your profile

setUsername(e.target.value)} />
setWebsite(e.target.value)} />
) } export default Account ``` You'll see the use of `useAuth()` several times. Redwood's `useAuth` hook provides convenient ways to access `logIn`, `logOut`, `currentUser`, and access the `supabase` authenticate client. We'll use it to get an instance of the Supabase client to interact with your API. ### Update home page Now that we have all the components in place, let's update your `HomePage` page to use them: <$CodeTabs> ```jsx name=web/src/pages/HomePage/HomePage.js import { useAuth } from '@redwoodjs/auth' import { MetaTags } from '@redwoodjs/web' import Account from 'src/components/Account' import Auth from 'src/components/Auth' const HomePage = () => { const { isAuthenticated } = useAuth() return ( <> {!isAuthenticated ? : } ) } export default HomePage ``` What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are. ### Launch! Once that's done, run this in a terminal window to launch the `dev` server: ```bash yarn rw dev ``` And then open the browser to [localhost:8910](http://localhost:8910) and you should see the completed app. ![Supabase RedwoodJS](/docs/img/supabase-redwoodjs-demo.png) ## Bonus: Profile photos Every Supabase project is configured with [Storage](/docs/guides/storage) for managing large files like photos and videos. ### Create an upload widget Let's create an avatar for the user so that they can upload a profile photo. We can start by creating a new component: ```bash yarn rw g component avatar ✔ Generating component files... ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.test.js` ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.stories.js` ✔ Successfully wrote file `./web/src/components/Avatar/Avatar.js` ``` Now, update your Avatar component to contain the following widget: <$CodeTabs> ```jsx name=web/src/components/Avatar/Avatar.js import { useEffect, useState } from 'react' import { useAuth } from '@redwoodjs/auth' const Avatar = ({ url, size, onUpload }) => { const { client: supabase } = useAuth() const [avatarUrl, setAvatarUrl] = useState(null) const [uploading, setUploading] = useState(false) useEffect(() => { if (url) downloadImage(url) }, [url]) async function downloadImage(path) { try { const { data, error } = await supabase.storage.from('avatars').download(path) if (error) { throw error } const url = URL.createObjectURL(data) setAvatarUrl(url) } catch (error) { console.log('Error downloading image: ', error.message) } } async function uploadAvatar(event) { try { setUploading(true) if (!event.target.files || event.target.files.length === 0) { throw new Error('You must select an image to upload.') } const file = event.target.files[0] const fileExt = file.name.split('.').pop() const fileName = `${Math.random()}.${fileExt}` const filePath = `${fileName}` const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file) if (uploadError) { throw uploadError } onUpload(filePath) } catch (error) { alert(error.message) } finally { setUploading(false) } } return (
{avatarUrl ? ( Avatar ) : (
)}
) } export default Avatar ``` ### Add the new widget And then we can add the widget to the Account component: <$CodeTabs> ```jsx name=web/src/components/Account/Account.js // Import the new component import Avatar from 'src/components/Avatar' // ... return (
{/* Add to the body */} { setAvatarUrl(url) updateProfile({ username, website, avatar_url: url }) }} /> {/* ... */}
) ``` At this stage you have a fully functional application! ## See also - Learn more about [RedwoodJS](https://redwoodjs.com) - Visit the [RedwoodJS Discourse Community](https://community.redwoodjs.com)