Files
supabase/apps/docs/content/guides/getting-started/tutorials/with-redwoodjs.mdx
Stojan Dimitrovski 93ba2a312c docs: indicate publishable key instead of anon in many examples (#37411)
* docs: indicate publishable key instead of anon in many examples

* replace your-anon-key to string indicating publishable or anon

* fix your_...

* apply suggestion from @ChrisChinchilla

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>

* Update keys in code examples

* Prettier fix

* Update apps/docs/content/guides/functions/schedule-functions.mdx

---------

Co-authored-by: Chris Chinchilla <chris@chrischinchilla.com>
2025-08-18 13:47:48 +02:00

652 lines
18 KiB
Plaintext

---
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)
<Admonition type="note">
If you get stuck while working through this guide, refer to the [full example on GitHub](https://github.com/redwoodjs/redwoodjs-supabase-quickstart).
</Admonition>
## 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.
<Admonition type="note">
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`.
</Admonition>
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.
<Admonition type="note">
RedwoodJS requires Node.js `>= 14.x <= 16.x` and Yarn `>= 1.15`.
</Admonition>
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
```
</$CodeTabs>
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
```
</$CodeTabs>
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 = () => (
<FatalErrorBoundary page={FatalErrorPage}>
<RedwoodProvider titleTemplate="%PageTitle | %AppTitle">
<AuthProvider client={supabase} type="supabase">
<RedwoodApolloProvider>
<Routes />
</RedwoodApolloProvider>
</AuthProvider>
</RedwoodProvider>
</FatalErrorBoundary>
)
export default App
```
</$CodeTabs>
### 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
```
<Admonition type="note">
`rw` is an alias for `redwood`, as in `yarn rw` to run Redwood CLI commands.
</Admonition>
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 ...
```
<Admonition type="note">
The `/` is important here as it creates a root level route.
</Admonition>
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 (
<Router>
<Route path="/" page={HomePage} name="home" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
export default Routes
```
</$CodeTabs>
### 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 (
<div className="row flex-center flex">
<div className="col-6 form-widget">
<h1 className="header">Supabase + RedwoodJS</h1>
<p className="description">Sign in via magic link with your email below</p>
<div>
<input
className="inputField"
type="email"
placeholder="Your email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<button
onClick={(e) => {
e.preventDefault()
handleLogin(email)
}}
className={'button block'}
disabled={loading}
>
{loading ? <span>Loading</span> : <span>Send magic link</span>}
</button>
</div>
</div>
</div>
)
}
export default Auth
```
</$CodeTabs>
### 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 (
<div className="row flex-center flex">
<div className="col-6 form-widget">
<h1 className="header">Supabase + RedwoodJS</h1>
<p className="description">Your profile</p>
<div className="form-widget">
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={currentUser.email} disabled />
</div>
<div>
<label htmlFor="username">Name</label>
<input
id="username"
type="text"
value={username || ''}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div>
<label htmlFor="website">Website</label>
<input
id="website"
type="url"
value={website || ''}
onChange={(e) => setWebsite(e.target.value)}
/>
</div>
<div>
<button
className="button primary block"
onClick={() => updateProfile({ username, website, avatar_url })}
disabled={loading}
>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<button className="button block" onClick={() => logOut()}>
Sign Out
</button>
</div>
</div>
</div>
</div>
)
}
export default Account
```
</$CodeTabs>
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 (
<>
<MetaTags title="Welcome" />
{!isAuthenticated ? <Auth /> : <Account />}
</>
)
}
export default HomePage
```
</$CodeTabs>
<Admonition type="note">
What we're doing here is showing the sign in form if you aren't logged in and your account profile if you are.
</Admonition>
### 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 (
<div>
{avatarUrl ? (
<img
src={avatarUrl}
alt="Avatar"
className="avatar image"
style={{ height: size, width: size }}
/>
) : (
<div className="avatar no-image" style={{ height: size, width: size }} />
)}
<div style={{ width: size }}>
<label className="button primary block" htmlFor="single">
{uploading ? 'Uploading ...' : 'Upload'}
</label>
<input
style={{
visibility: 'hidden',
position: 'absolute',
}}
type="file"
id="single"
accept="image/*"
onChange={uploadAvatar}
disabled={uploading}
/>
</div>
</div>
)
}
export default Avatar
```
</$CodeTabs>
### 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 (
<div className="form-widget">
{/* Add to the body */}
<Avatar
url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url)
updateProfile({ username, website, avatar_url: url })
}}
/>
{/* ... */}
</div>
)
```
</$CodeTabs>
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)