Files
supabase/apps/docs/content/guides/getting-started/tutorials/with-nextjs.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

1035 lines
25 KiB
Plaintext

---
title: 'Build a User Management App with Next.js'
description: 'Learn how to use Supabase in your Next.js 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/supabase/supabase/tree/master/examples/user-management/nextjs-user-management).
</Admonition>
<$Partial path="project_setup.mdx" />
## Building the app
Start building the Next.js app from scratch.
### Initialize a Next.js app
Use [`create-next-app`](https://nextjs.org/docs/getting-started) to initialize an app called `supabase-nextjs`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```bash
npx create-next-app@latest --use-npm supabase-nextjs
cd supabase-nextjs
```
</TabPanel>
<TabPanel id="ts" label="TypeScript">
```bash
npx create-next-app@latest --ts --use-npm supabase-nextjs
cd supabase-nextjs
```
</TabPanel>
</Tabs>
Then install the Supabase client library: [supabase-js](https://github.com/supabase/supabase-js)
```bash
npm install @supabase/supabase-js
```
Save the environment variables in a `.env.local` file at the root of the project, and paste the API URL and the `anon` key that you copied [earlier](#get-the-api-keys).
```bash .env.local
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=YOUR_SUPABASE_PUBLISHABLE_KEY
```
### App styling (optional)
An optional step is to update the CSS file `app/globals.css` to make the app look nice.
You can find the full contents of this file [in the example repository](https://raw.githubusercontent.com/supabase/supabase/master/examples/user-management/nextjs-user-management/app/globals.css).
### Supabase Server-Side Auth
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.
To better integrate with the framework, we've created the `@supabase/ssr` package for Server-Side Auth. It has all the functionalities to quickly configure your Supabase project to use cookies for storing user sessions. Read the [Next.js Server-Side Auth guide](/docs/guides/auth/server-side/nextjs) for more information.
Install the package for Next.js.
```bash
npm install @supabase/ssr
```
### Supabase utilities
There are two different types of clients in Supabase:
1. **Client Component client** - To access Supabase from Client Components, which run in the browser.
2. **Server Component client** - To access Supabase from Server Components, Server Actions, and Route Handlers, which run only on the server.
It is recommended to create the following essential utilities files for creating clients, and organize them within `utils/supabase` at the root of the project.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Create a `client.js` and a `server.js` with the following functionalities for client-side Supabase and server-side Supabase, respectively.
<$CodeTabs>
```jsx name=utils/supabase/client.js
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
// Create a supabase client on the browser with project's credentials
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
)
}
```
```jsx name=utils/supabase/server.js
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export async function createClient() {
const cookieStore = await cookies()
// Create a server's supabase client with newly configured cookie,
// which could be used to maintain user's session
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
{
cookies: {
getAll() {
return cookieStore.getAll()
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
)
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing
// user sessions.
}
},
},
}
)
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a `client.ts` and a `server.ts` with the following functionalities for client-side Supabase and server-side Supabase, respectively.
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/utils/supabase/client.ts"
lines={[[1, -1]]}
meta="name=utils/supabase/client.ts"
/>
<$CodeSample
path="/user-management/nextjs-user-management/utils/supabase/server.ts"
lines={[[1, -1]]}
meta="name=utils/supabase/server.ts"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Next.js middleware
Since Server Components can't write cookies, you need middleware to refresh expired Auth tokens and store them. This is accomplished by:
- Refreshing the Auth token with the call to `supabase.auth.getUser`.
- Passing the refreshed Auth token to Server Components through `request.cookies.set`, so they don't attempt to refresh the same token themselves.
- Passing the refreshed Auth token to the browser, so it replaces the old token. This is done with `response.cookies.set`.
You could also add a matcher, so that the middleware only runs on routes that access Supabase. For more information, read [the Next.js matcher documentation](https://nextjs.org/docs/app/api-reference/file-conventions/middleware#matcher).
<Admonition type="danger">
Be careful when protecting pages. The server gets the user session from the cookies, which anyone can spoof.
Always use `supabase.auth.getUser()` to protect pages and user data.
_Never_ trust `supabase.auth.getSession()` inside server code such as middleware. It isn't guaranteed to revalidate the Auth token.
It's safe to trust `getUser()` because it sends a request to the Supabase Auth server every time to revalidate the Auth token.
</Admonition>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Create a `middleware.js` file at the project root and another one within the `utils/supabase` folder. The `utils/supabase` file contains the logic for updating the session. This is used by the `middleware.js` file, which is a Next.js convention.
<$CodeTabs>
```jsx name=middleware.js
import { updateSession } from '@/utils/supabase/middleware'
export async function middleware(request) {
// update user's auth session
return await updateSession(request)
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* Feel free to modify this pattern to include more paths.
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
```
```jsx name=utils/supabase/middleware.js
import { createServerClient } from '@supabase/ssr'
import { NextResponse } from 'next/server'
export async function updateSession(request) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
supabaseResponse = NextResponse.next({
request,
})
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
)
},
},
}
)
// refreshing the auth token
await supabase.auth.getUser()
return supabaseResponse
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a `middleware.ts` file at the project root and another one within the `utils/supabase` folder. The `utils/supabase` file contains the logic for updating the session. This is used by the `middleware.ts` file, which is a Next.js convention.
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/middleware.ts"
lines={[[1, -1]]}
meta="name=middleware.ts"
/>
<$CodeSample
path="/user-management/nextjs-user-management/utils/supabase/middleware.ts"
lines={[[1, -1]]}
meta="name=utils/supabase/middleware.ts"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
## Set up a login page
### Login and signup form
Create a login/signup page for your application:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Create a new folder named `login`, containing a `page.jsx` file with a login/signup form.
<$CodeTabs>
```jsx name=app/login/page.jsx
import { login, signup } from './actions'
export default function LoginPage() {
return (
<form>
<label htmlFor="email">Email:</label>
<input id="email" name="email" type="email" required />
<label htmlFor="password">Password:</label>
<input id="password" name="password" type="password" required />
<button formAction={login}>Log in</button>
<button formAction={signup}>Sign up</button>
</form>
)
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create a new folder named `login`, containing a `page.tsx` file with a login/signup form.
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/login/page.tsx"
lines={[[1, -1]]}
meta="name=app/login/page.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
Next, you need to create the login/signup actions to hook up the form to the function. Which does the following:
- Retrieve the user's information.
- Send that information to Supabase as a signup request, which in turns sends a confirmation email.
- Handle any error that arises.
<Admonition type="caution">
The `cookies` method is called before any calls to Supabase, which takes fetch calls out of Next.js's caching. This is important for authenticated data fetches, to ensure that users get access only to their own data.
Read the Next.js docs to learn more about [opting out of data caching](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#opting-out-of-data-caching).
</Admonition>
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
Create the `action.js` file in the `app/login` folder, which contains the login and signup functions and the `error/page.jsx` file, and displays an error message if the login or signup fails.
<$CodeTabs>
```js name=app/login/actions.js
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'
export async function login(formData) {
const supabase = await createClient()
// type-casting here for convenience
// in practice, you should validate your inputs
const data = {
email: formData.get('email'),
password: formData.get('password'),
}
const { error } = await supabase.auth.signInWithPassword(data)
if (error) {
redirect('/error')
}
revalidatePath('/', 'layout')
}
export async function signup(formData) {
const supabase = await createClient()
const data = {
email: formData.get('email'),
password: formData.get('password'),
}
const { error } = await supabase.auth.signUp(data)
if (error) {
redirect('/error')
}
revalidatePath('/', 'layout')
}
```
```jsx name=app/error/page.jsx
export default function ErrorPage() {
return <p>Sorry, something went wrong</p>
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
Create the `action.ts` file in the `app/login` folder, which contains the login and signup functions and the `error/page.tsx` file, which displays an error message if the login or signup fails.
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/login/actions.ts"
lines={[[1, -1]]}
meta="name=app/login/actions.ts"
/>
<$CodeSample
path="/user-management/nextjs-user-management/app/error/page.tsx"
lines={[[1, -1]]}
meta="name=app/error/page.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Email template
Before proceeding, change the email template to support support a server-side authentication flow that sends a token hash:
- Go to the [Auth templates](/dashboard/project/_/auth/templates) page in your dashboard.
- Select the **Confirm signup** template.
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
<Admonition type="tip">
**Did you know?** You can also customize other emails sent out to new users, including the email's looks, content, and query parameters. Check out the [settings of your project](/dashboard/project/_/auth/templates).
</Admonition>
### Confirmation endpoint
As you are working in a server-side rendering (SSR) environment, you need to create a server endpoint responsible for exchanging the `token_hash` for a session.
The code performs the following steps:
- Retrieves the code sent back from the Supabase Auth server using the `token_hash` query parameter.
- Exchanges this code for a session, which you store in your chosen storage mechanism (in this case, cookies).
- Finally, redirects the user to the `account` page.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```js name=app/auth/confirm/route.js
import { NextResponse } from 'next/server'
import { createClient } from '@/utils/supabase/server'
// Creating a handler to a GET request to route /auth/confirm
export async function GET(request) {
const { searchParams } = new URL(request.url)
const token_hash = searchParams.get('token_hash')
const type = searchParams.get('type')
const next = '/account'
// Create redirect link without the secret token
const redirectTo = request.nextUrl.clone()
redirectTo.pathname = next
redirectTo.searchParams.delete('token_hash')
redirectTo.searchParams.delete('type')
if (token_hash && type) {
const supabase = await createClient()
const { error } = await supabase.auth.verifyOtp({
type,
token_hash,
})
if (!error) {
redirectTo.searchParams.delete('next')
return NextResponse.redirect(redirectTo)
}
}
// return the user to an error page with some instructions
redirectTo.pathname = '/error'
return NextResponse.redirect(redirectTo)
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/auth/confirm/route.ts"
lines={[[1, -1]]}
meta="name=app/auth/confirm/route.ts"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Account page
After a user signs in, allow them to edit their profile details and manage their account.
Create a new component for that called `AccountForm` within the `app/account` folder.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```jsx name=app/account/account-form.jsx
'use client'
import { useCallback, useEffect, useState } from 'react'
import { createClient } from '@/utils/supabase/client'
export default function AccountForm({ user }) {
const supabase = createClient()
const [loading, setLoading] = useState(true)
const [fullname, setFullname] = useState(null)
const [username, setUsername] = useState(null)
const [website, setWebsite] = useState(null)
const getProfile = useCallback(async () => {
try {
setLoading(true)
const { data, error, status } = await supabase
.from('profiles')
.select(`full_name, username, website, avatar_url`)
.eq('id', user?.id)
.single()
if (error && status !== 406) {
throw error
}
if (data) {
setFullname(data.full_name)
setUsername(data.username)
setWebsite(data.website)
}
} catch (error) {
alert('Error loading user data!')
} finally {
setLoading(false)
}
}, [user, supabase])
useEffect(() => {
getProfile()
}, [user, getProfile])
async function updateProfile({ username, website, avatar_url }) {
try {
setLoading(true)
const { error } = await supabase.from('profiles').upsert({
id: user?.id,
full_name: fullname,
username,
website,
updated_at: new Date().toISOString(),
})
if (error) throw error
alert('Profile updated!')
} catch (error) {
alert('Error updating the data!')
} finally {
setLoading(false)
}
}
return (
<div className="form-widget">
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={user?.email} disabled />
</div>
<div>
<label htmlFor="fullName">Full Name</label>
<input
id="fullName"
type="text"
value={fullname || ''}
onChange={(e) => setFullname(e.target.value)}
/>
</div>
<div>
<label htmlFor="username">Username</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({ fullname, username, website })}
disabled={loading}
>
{loading ? 'Loading ...' : 'Update'}
</button>
</div>
<div>
<form action="/auth/signout" method="post">
<button className="button block" type="submit">
Sign out
</button>
</form>
</div>
</div>
)
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/account/account-form.tsx"
lines={[[1, 4], [7, 78], [88, -1]]}
meta="name=app/account/account-form.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
Create an account page for the `AccountForm` component you just created
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```jsx name=app/account/page.jsx
import AccountForm from './account-form'
import { createClient } from '@/utils/supabase/server'
export default async function Account() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
return <AccountForm user={user} />
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/account/page.tsx"
lines={[[1, -1]]}
meta="name=app/account/page.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Sign out
Create a route handler to handle the sign out from the server side, making sure to check if the user is logged in first.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```js name=app/auth/signout/route.js
import { createClient } from '@/utils/supabase/server'
import { revalidatePath } from 'next/cache'
import { NextResponse } from 'next/server'
export async function POST(req) {
const supabase = await createClient()
// Check if a user's logged in
const {
data: { user },
} = await supabase.auth.getUser()
if (user) {
await supabase.auth.signOut()
}
revalidatePath('/', 'layout')
return NextResponse.redirect(new URL('/login', req.url), {
status: 302,
})
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/auth/signout/route.ts"
lines={[[1, -1]]}
meta="name=app/auth/signout/route.ts"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Launch!
Now you have all the pages, route handlers, and components in place, run the following in a terminal window:
```bash
npm run dev
```
And then open the browser to [localhost:3000/login](http://localhost:3000/login) and you should see the completed app.
When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!!
## 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
Create an avatar widget for the user so that they can upload a profile photo. Start by creating a new component:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```jsx name=app/account/avatar.jsx
'use client'
import React, { useEffect, useState } from 'react'
import { createClient } from '@/utils/supabase/client'
import Image from 'next/image'
export default function Avatar({ uid, url, size, onUpload }) {
const supabase = createClient()
const [avatarUrl, setAvatarUrl] = useState(url)
const [uploading, setUploading] = useState(false)
useEffect(() => {
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)
}
}
if (url) downloadImage(url)
}, [url, supabase])
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 filePath = `${uid}-${Math.random()}.${fileExt}`
const { error: uploadError } = await supabase.storage.from('avatars').upload(filePath, file)
if (uploadError) {
throw uploadError
}
onUpload(filePath)
} catch (error) {
alert('Error uploading avatar!')
} finally {
setUploading(false)
}
}
return (
<div>
{avatarUrl ? (
<Image
width={size}
height={size}
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>
)
}
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/account/avatar.tsx"
lines={[[1, -1]]}
meta="name=app/account/avatar.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
### Add the new widget
Then add the widget to the `AccountForm` component:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
<$CodeTabs>
```jsx name=app/account/account-form.jsx
// Import the new component
import Avatar from './avatar'
// ...
return (
<div className="form-widget">
{/* Add to the body */}
<Avatar
uid={user?.id}
url={avatar_url}
size={150}
onUpload={(url) => {
setAvatarUrl(url)
updateProfile({ fullname, username, website, avatar_url: url })
}}
/>
{/* ... */}
</div>
)
```
</$CodeTabs>
</TabPanel>
<TabPanel id="ts" label="TypeScript">
<$CodeTabs>
<$CodeSample
path="/user-management/nextjs-user-management/app/account/account-form.tsx"
lines={[[5, 5], [77, 87], [137, -1]]}
meta="name=app/account/account-form.tsx"
/>
</$CodeTabs>
</TabPanel>
</Tabs>
At this stage you have a fully functional application!
## See also
- See the complete [example on GitHub](https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-user-management) and deploy it to Vercel
- [Build a Twitter Clone with the Next.js App Router and Supabase - free egghead course](https://egghead.io/courses/build-a-twitter-clone-with-the-next-js-app-router-and-supabase-19bebadb)
- Explore the [pre-built Auth UI for React](/docs/guides/auth/auth-helpers/auth-ui)
- Explore the [Auth Helpers for Next.js](/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