Quickstart: Next.js

Intro#

This example provides the steps to build a basic user management app. It includes:

  • Supabase Database: a Postgres database for storing your user data.
  • Supabase Auth: users can sign in with magic links (no passwords, only email).
  • Supabase Storage: users can upload a photo.
  • Row Level Security: data is protected so that individuals can only access their own data.
  • Instant APIs: APIs will be automatically generated when you create your database tables.

By the end of this guide you'll have an app which allows users to login and update some basic profile details:

Supabase User Management example

Video Guide#

GitHub#

Should you get stuck while working through the guide, refer to this repo.

Project set up#

Before we start building we're going to set up our Database and API. This is as simple as starting a new Project in Supabase and then creating a "schema" inside the database.

Create a project#

  1. Go to app.supabase.com.
  2. Click on "New Project".
  3. Enter your project details.
  4. Wait for the new database to launch.

Set up the database schema#

Now we are going to set up the database schema. We can use the "User Management Starter" quickstart in the SQL Editor, or you can just copy/paste the SQL from below and run it yourself.

  1. Go to the SQL Editor page in the Dashboard.
  2. Click User Management Starter.
  3. Click Run.

Get the API Keys#

Now that you've created some database tables, you are ready to insert data using the auto-generated API. We just need to get the URL and anon key from the API settings.

  1. Go to the Settings page in the Dashboard.
  2. Click API in the sidebar.
  3. Find your API URL, anon, and service_role keys on this page.

Building the App#

Let's start building the Next.js app from scratch.

Initialize a Next.js app#

We can use create-next-app to initialize an app called supabase-nextjs:

npx create-next-app@latest --use-npm supabase-nextjs
cd supabase-nextjs

Then install the Supabase client library: supabase-js

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.

.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.

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 to make user management and data fetching within Next.js as easy as possible.

Install the auth helpers for React and Next.js

npm install @supabase/auth-helpers-react @supabase/auth-helpers-nextjs

Wrap your pages/_app.js component with the SessionContextProvider component:

pages/_app.js
1import '../styles/globals.css'
2import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
3import { SessionContextProvider } from '@supabase/auth-helpers-react'
4
5function MyApp({ Component, pageProps }) {
6  const [supabase] = useState(() => createBrowserSupabaseClient())
7
8  return (
9    <SessionContextProvider
10      supabaseClient={supabase}
11      initialSession={pageProps.initialSession}
12    >
13      <Component {...pageProps} />
14    </SessionContextProvider>
15  )
16}
17export default MyApp

Supabase Auth UI

We can use the Supabase Auth UI a pre-built React component for authenticating users via OAuth, email, and magic links.

Install the Supabase Auth UI for React

npm install @supabase/auth-ui-react

Add the Auth component to your home page

pages/index.js
1import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
2import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
3
4const Home = () => {
5  const session = useSession()
6  const supabase = useSupabaseClient()
7
8  return (
9    <div className="container" style={{ padding: '50px 0 100px 0' }}>
10      {!session ? (
11        <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
12      ) : (
13        <p>Account page will go here.</p>
14      )}
15    </div>
16  )
17}
18
19export 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.

components/Account.js
1import { useState, useEffect } from 'react'
2import { useUser, useSupabaseClient } from '@supabase/auth-helpers-react'
3
4export default function Account({ session }) {
5  const supabase = useSupabaseClient()
6  const user = useUser()
7  const [loading, setLoading] = useState(true)
8  const [username, setUsername] = useState(null)
9  const [website, setWebsite] = useState(null)
10  const [avatar_url, setAvatarUrl] = useState(null)
11
12  useEffect(() => {
13    getProfile()
14  }, [session])
15
16  async function getProfile() {
17    try {
18      setLoading(true)
19
20      let { data, error, status } = await supabase
21        .from('profiles')
22        .select(`username, website, avatar_url`)
23        .eq('id', user.id)
24        .single()
25
26      if (error && status !== 406) {
27        throw error
28      }
29
30      if (data) {
31        setUsername(data.username)
32        setWebsite(data.website)
33        setAvatarUrl(data.avatar_url)
34      }
35    } catch (error) {
36      alert('Error loading user data!')
37      console.log(error)
38    } finally {
39      setLoading(false)
40    }
41  }
42
43  async function updateProfile({ username, website, avatar_url }) {
44    try {
45      setLoading(true)
46
47      const updates = {
48        id: user.id,
49        username,
50        website,
51        avatar_url,
52        updated_at: new Date().toISOString(),
53      }
54
55      let { error } = await supabase.from('profiles').upsert(updates)
56      if (error) throw error
57      alert('Profile updated!')
58    } catch (error) {
59      alert('Error updating the data!')
60      console.log(error)
61    } finally {
62      setLoading(false)
63    }
64  }
65
66  return (
67    <div className="form-widget">
68      <div>
69        <label htmlFor="email">Email</label>
70        <input id="email" type="text" value={session.user.email} disabled />
71      </div>
72      <div>
73        <label htmlFor="username">Username</label>
74        <input
75          id="username"
76          type="text"
77          value={username || ''}
78          onChange={(e) => setUsername(e.target.value)}
79        />
80      </div>
81      <div>
82        <label htmlFor="website">Website</label>
83        <input
84          id="website"
85          type="website"
86          value={website || ''}
87          onChange={(e) => setWebsite(e.target.value)}
88        />
89      </div>
90
91      <div>
92        <button
93          className="button primary block"
94          onClick={() => updateProfile({ username, website, avatar_url })}
95          disabled={loading}
96        >
97          {loading ? 'Loading ...' : 'Update'}
98        </button>
99      </div>
100
101      <div>
102        <button className="button block" onClick={() => supabase.auth.signOut()}>
103          Sign Out
104        </button>
105      </div>
106    </div>
107  )
108}

Launch!#

Now that we have all the components in place, let's update pages/index.js:

pages/index.js
1import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
2import { useSession, useSupabaseClient } from '@supabase/auth-helpers-react'
3import Account from '../components/Account'
4
5const Home = () => {
6  const session = useSession()
7  const supabase = useSupabaseClient()
8
9  return (
10    <div className="container" style={{ padding: '50px 0 100px 0' }}>
11      {!session ? (
12        <Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
13      ) : (
14        <Account session={session} />
15      )}
16    </div>
17  )
18}
19
20export default Home

Once that's done, run this in a terminal window:

npm run dev

And then open the browser to localhost:3000 and you should see the completed app.

Bonus: Profile photos#

Every Supabase project is configured with 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:

components/Avatar.js
1import React, { useEffect, useState } from 'react'
2import { useSupabaseClient } from '@supabase/auth-helpers-react'
3
4export default function Avatar({ uid, url, size, onUpload }) {
5  const supabase = useSupabaseClient()
6  const [avatarUrl, setAvatarUrl] = useState(null)
7  const [uploading, setUploading] = useState(false)
8
9  useEffect(() => {
10    if (url) downloadImage(url)
11  }, [url])
12
13  async function downloadImage(path) {
14    try {
15      const { data, error } = await supabase.storage.from('avatars').download(path)
16      if (error) {
17        throw error
18      }
19      const url = URL.createObjectURL(data)
20      setAvatarUrl(url)
21    } catch (error) {
22      console.log('Error downloading image: ', error)
23    }
24  }
25
26  const uploadAvatar = async (event) => {
27    try {
28      setUploading(true)
29
30      if (!event.target.files || event.target.files.length === 0) {
31        throw new Error('You must select an image to upload.')
32      }
33
34      const file = event.target.files[0]
35      const fileExt = file.name.split('.').pop()
36      const fileName = `${uid}.${fileExt}`
37      const filePath = `${fileName}`
38
39      let { error: uploadError } = await supabase.storage
40        .from('avatars')
41        .upload(filePath, file, { upsert: true })
42
43      if (uploadError) {
44        throw uploadError
45      }
46
47      onUpload(filePath)
48    } catch (error) {
49      alert('Error uploading avatar!')
50      console.log(error)
51    } finally {
52      setUploading(false)
53    }
54  }
55
56  return (
57    <div>
58      {avatarUrl ? (
59        <img
60          src={avatarUrl}
61          alt="Avatar"
62          className="avatar image"
63          style={{ height: size, width: size }}
64        />
65      ) : (
66        <div className="avatar no-image" style={{ height: size, width: size }} />
67      )}
68      <div style={{ width: size }}>
69        <label className="button primary block" htmlFor="single">
70          {uploading ? 'Uploading ...' : 'Upload'}
71        </label>
72        <input
73          style={{
74            visibility: 'hidden',
75            position: 'absolute',
76          }}
77          type="file"
78          id="single"
79          accept="image/*"
80          onChange={uploadAvatar}
81          disabled={uploading}
82        />
83      </div>
84    </div>
85  )
86}

Add the new widget#

And then we can add the widget to the Account page:

components/Account.js
1// Import the new component
2import Avatar from './Avatar'
3
4// ...
5
6return (
7  <div className="form-widget">
8    {/* Add to the body */}
9    <Avatar
10      uid={user.id}
11      url={avatar_url}
12      size={150}
13      onUpload={(url) => {
14        setAvatarUrl(url)
15        updateProfile({ username, website, avatar_url: url })
16      }}
17    />
18    {/* ... */}
19  </div>
20)

Next steps#

At this stage you have a fully functional application!