import Layout from '~/layouts/tutorials/TutorialLayout' export const meta = { title: 'Creating a user management dashboard with NextJS and Supabase', author: 'Rich Haines', video: 'https://www.youtube-nocookie.com/embed/0Fs96oZ4se0', } In this 25-minute guide, we’ll be building a collaborative user managment app using NextJS and Supabase. As users add and move rectangles in a canvas, changes will be automatically synced and persisted, allowing for a canvas that updates in real-time across clients. Users will also be able to see other users selections, and undo and redo actions. ![Supabase User Management example](/docs/img/user-management-demo.png) ### GitHub Should you get stuck while working through the guide, refer to [this repo](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management). ## Building the App Let's start building the Next.js app from scratch. ### Initialize a Next.js app We can use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`: ```bash npx create-next-app@latest --use-npm supabase-nextjs cd supabase-nextjs ``` ```bash npx create-next-app@latest --ts --use-npm supabase-nextjs cd supabase-nextjs ``` Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js) ```bash npm install @supabase/supabase-js ``` And finally we want to save the environment variables in a `.env.local`. All we need are the API URL and the `anon` key that you copied [earlier](#get-the-api-keys). ```bash title=.env.local NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY ``` And one optional step is to update the CSS file `styles/globals.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/nextjs-ts-user-management/styles/globals.css). ### Set up a Login component #### Supabase Auth Helpers Next.js is a highly versatile framework offering pre-rendering at build time (SSG), server-side rendering at request time (SSR), API routes, and middleware edge-functions. It can be challenging to authenticate your users in all these different environments, that's why we've created the [Supabase Auth Helpers](https://supabase.com/docs/guides/auth/auth-helpers/nextjs) to make user management and data fetching within Next.js as easy as possible. Install the auth helpers for React and Next.js ```bash npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs ``` Wrap your `pages/_app.js` component with the `SessionContextProvider` component: ```jsx title=pages/_app.js import '../styles/globals.css' import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider } from '@supabase/auth-helpers-react' function MyApp({ Component, pageProps }) { const [supabaseClient] = useState(() => createBrowserSupabaseClient()) return ( ) } export default MyApp ``` Wrap your `pages/_app.tsx` component with the `SessionContextProvider` component: ```jsx lines=2,8 title=pages/_app.tsx import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs' import { SessionContextProvider, Session } from '@supabase/auth-helpers-react' function MyApp({ Component, pageProps, }: AppProps<{ initialSession: Session, }>) { const [supabaseClient] = useState(() => createBrowserSupabaseClient()) return ( ) } export default MyApp ``` See the [Auth Helpers docs](/docs/guides/auth/auth-helpers/nextjs#usage-with-typescript) for more details on usage with TypeScript. #### Supabase Auth UI We can use the [Supabase Auth UI](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui) a pre-built React component for authenticating users via OAuth, email, and magic links. Install the Supabase Auth UI for React ```bash npm install @supabase/auth-ui-react ``` Add the `Auth` component to your home page ```jsx title=pages/index.js import { Auth, ThemeSupa } from '@supabase/auth-ui-react' import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react' const Home = () => { const session = useSession() const supabase = useSupabaseClient() return (
{!session ? ( ) : (

Account page will go here.

)}
) } export default Home ``` ### Account page 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` within a `components` folder. ```tsx title=components/Account.js import { useState, useEffect } from 'react' import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react' export default function Account({ session }) { const supabase = useSupabaseClient() const user = useUser() const [loading, setLoading] = useState(true) const [username, setUsername] = useState(null) const [website, setWebsite] = useState(null) const [avatar_url, setAvatarUrl] = useState(null) useEffect(() => { getProfile() }, [session]) async function getProfile() { try { setLoading(true) let { 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 loading user data!') console.log(error) } finally { setLoading(false) } } async function updateProfile({ username, website, avatar_url }) { try { setLoading(true) const updates = { id: user.id, username, website, avatar_url, updated_at: new Date().toISOString(), } let { error } = await supabase.from('profiles').upsert(updates) if (error) throw error alert('Profile updated!') } catch (error) { alert('Error updating the data!') console.log(error) } finally { setLoading(false) } } return (
setUsername(e.target.value)} />
setWebsite(e.target.value)} />
) } ```
Let's create a new component for that called `Account.tsx` within a `components` folder. ```tsx title=components/Account.tsx import { useState, useEffect } from 'react' import { useUser, useSupabaseClient, Session } from '@supabase/auth-helpers-react' import { Database } from '../utils/database.types' type Profiles = Database['public']['Tables']['profiles']['Row'] export default function Account({ session }: { session: Session }) { const supabase = useSupabaseClient() const user = useUser() const [loading, setLoading] = useState(true) const [username, setUsername] = useState(null) const [website, setWebsite] = useState(null) const [avatar_url, setAvatarUrl] = useState(null) useEffect(() => { getProfile() }, [session]) async function getProfile() { try { setLoading(true) if (!user) throw new Error('No user') let { 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 loading user data!') console.log(error) } finally { setLoading(false) } } async function updateProfile({ username, website, avatar_url, }: { username: Profiles['username'] website: Profiles['website'] avatar_url: Profiles['avatar_url'] }) { try { setLoading(true) if (!user) throw new Error('No user') const updates = { id: user.id, username, website, avatar_url, updated_at: new Date().toISOString(), } let { error } = await supabase.from('profiles').upsert(updates) if (error) throw error alert('Profile updated!') } catch (error) { alert('Error updating the data!') console.log(error) } finally { setLoading(false) } } return (
setUsername(e.target.value)} />
setWebsite(e.target.value)} />
) } ```
### Launch! Now that we have all the components in place, let's update `pages/index.js`: ```jsx lines=3,14 title=pages/index.js import { Auth, ThemeSupa } from '@supabase/auth-ui-react' import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react' import Account from '../components/Account' const Home = () => { const session = useSession() const supabase = useSupabaseClient() return (
{!session ? ( ) : ( )}
) } export default Home ``` Once that's done, run this in a terminal window: ```bash npm run dev ``` And then open the browser to [localhost:3000](http://localhost:3000) and you should see the completed app. ## 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 widget for the user so that they can upload a profile photo. We can start by creating a new component: ```jsx title=components/Avatar.js import React, { useEffect, useState } from 'react' import { useSupabaseClient } from '@supabase/auth-helpers-react' export default function Avatar({ uid, url, size, onUpload }) { const supabase = useSupabaseClient() 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) } } const uploadAvatar = async (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 = `${uid}.${fileExt}` const filePath = `${fileName}` let { error: uploadError } = await supabase.storage .from('avatars') .upload(filePath, file, { upsert: true }) if (uploadError) { throw uploadError } onUpload(filePath) } catch (error) { alert('Error uploading avatar!') console.log(error) } finally { setUploading(false) } } return (
{avatarUrl ? ( Avatar ) : (
)}
) } ``` ```tsx title=components/Avatar.tsx import React, { useEffect, useState } from 'react' import { useSupabaseClient } from '@supabase/auth-helpers-react' import { Database } from '../utils/database.types' type Profiles = Database['public']['Tables']['profiles']['Row'] export default function Avatar({ uid, url, size, onUpload, }: { uid: string url: Profiles['avatar_url'] size: number onUpload: (url: string) => void }) { const supabase = useSupabaseClient() const [avatarUrl, setAvatarUrl] = useState(null) const [uploading, setUploading] = useState(false) useEffect(() => { if (url) downloadImage(url) }, [url]) async function downloadImage(path: string) { 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) } } const uploadAvatar: React.ChangeEventHandler = async (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 = `${uid}.${fileExt}` const filePath = `${fileName}` let { error: uploadError } = await supabase.storage .from('avatars') .upload(filePath, file, { upsert: true }) if (uploadError) { throw uploadError } onUpload(filePath) } catch (error) { alert('Error uploading avatar!') console.log(error) } finally { setUploading(false) } } return (
{avatarUrl ? ( Avatar ) : (
)}
) } ``` ### Add the new widget And then we can add the widget to the Account page: ```jsx title=components/Account.js // Import the new component import Avatar from './Avatar' // ... return (
{/* Add to the body */} { setAvatarUrl(url) updateProfile({ username, website, avatar_url: url }) }} /> {/* ... */}
) ``` ## Next steps At this stage you have a fully functional application! - See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management) and deploy it to Vercel. - Explore the [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-helpers/auth-ui). - Explore the [Auth Helpers for Next.js](https://supabase.com/docs/guides/auth/auth-helpers/nextjs). - Explore the [Supabase Cache Helpers](https://github.com/psteinroe/supabase-cache-helpers). - See the [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments) template on GitHub. - Got a question? [Ask here](https://github.com/supabase/supabase/discussions). - Sign in: [app.supabase.com](https://app.supabase.com) export default ({ children }) =>