docs: Update nextjs user management guide to use getClaims (#44124)

This commit is contained in:
Chris Chinchilla authored and GitHub committed 2026-03-24 21:22:41 +01:00
1 parent fa4a668eeb
commit 8e33378315
7 files changed
+1296 -1401

No files matched your search

@@ -44,7 +44,7 @@ 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.
An optional step is to update the CSS file `app/globals.css` to make the app look better.
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
@@ -66,7 +66,7 @@ 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 `lib/supabase` at the root of the project.
We recommend creating the following essential utilities files for creating clients, and organize them within `lib/supabase` at the root of the project.
Create a `client.ts` and a `server.ts` with the following functionalities for client-side Supabase and server-side Supabase, respectively.
@@ -88,9 +88,11 @@ meta="name=lib/supabase/server.ts"
### Next.js proxy
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them. This is accomplished by:
Since Server Components can't write cookies, you need [Proxy](https://nextjs.org/docs/app/getting-started/proxy) to refresh expired Auth tokens and store them.
- Refreshing the Auth token with the call to `supabase.auth.getUser`.
You can accomplish this by:
- Refreshing the Auth token with the call to `supabase.auth.getClaims`.
- 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`.
@@ -100,15 +102,15 @@ You could also add a matcher, so that the Proxy only runs on routes that access
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.
Most of the time, use `supabase.auth.getClaims()` to protect pages and user data.
_Never_ trust `supabase.auth.getSession()` inside server code such as proxy. 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.
It's safe to trust `getClaims()` because it validates the token in storage, either directly or by calling `getUser()` solely to check the result. It doesn't use the response from `getUser()` itself, only whether the validation succeeded.
</Admonition>
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. This is used by the `proxy.ts` file, which is a Next.js convention.
Create a `proxy.ts` file at the project root and another one within the `lib/supabase` folder. The `lib/supabase` file contains the logic for updating the session. The `proxy.ts` file uses this, which is a Next.js convention.
<$CodeTabs>
@@ -184,9 +186,9 @@ Before proceeding, change the email template to support support a server-side au
- Select the **Confirm signup** template.
- Change `{{ .ConfirmationURL }}` to `{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email`.
<Admonition type="tip">
<Admonition type="tip" label="Did you know?">
**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).
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>
@@ -1,10 +1,11 @@
'use client'
import { useCallback, useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import { type User } from '@supabase/supabase-js'
import Avatar from './avatar'
export default function AccountForm({ user }: { user: User | null }) {
type Claims = { sub: string; email?: string; [key: string]: unknown }
export default function AccountForm({ claims }: { claims: Claims | null }) {
const supabase = createClient()
const [loading, setLoading] = useState(true)
const [fullname, setFullname] = useState<string | null>(null)
@@ -14,12 +15,17 @@ export default function AccountForm({ user }: { user: User | null }) {
const getProfile = useCallback(async () => {
try {
if (!claims?.sub) {
setLoading(false)
return
}
setLoading(true)
const { data, error, status } = await supabase
.from('profiles')
.select(`full_name, username, website, avatar_url`)
.eq('id', user?.id)
.eq('id', claims.sub)
.single()
if (error && status !== 406) {
@@ -38,11 +44,11 @@ export default function AccountForm({ user }: { user: User | null }) {
} finally {
setLoading(false)
}
}, [user, supabase])
}, [claims, supabase])
useEffect(() => {
getProfile()
}, [user, getProfile])
}, [claims, getProfile])
async function updateProfile({
username,
@@ -55,10 +61,15 @@ export default function AccountForm({ user }: { user: User | null }) {
avatar_url: string | null
}) {
try {
if (!claims?.sub) {
alert('You must be logged in to update your profile')
return
}
setLoading(true)
const { error } = await supabase.from('profiles').upsert({
id: user?.id as string,
id: claims.sub,
full_name: fullname,
username,
website,
@@ -77,7 +88,7 @@ export default function AccountForm({ user }: { user: User | null }) {
return (
<div className="form-widget">
<Avatar
uid={user?.id ?? null}
uid={claims?.sub ?? null}
url={avatar_url}
size={150}
onUpload={(url) => {
@@ -87,7 +98,7 @@ export default function AccountForm({ user }: { user: User | null }) {
/>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="text" value={user?.email} disabled />
<input id="email" type="text" value={claims?.email ?? ''} disabled />
</div>
<div>
<label htmlFor="fullName">Full Name</label>
@@ -121,7 +132,7 @@ export default function AccountForm({ user }: { user: User | null }) {
<button
className="button primary block"
onClick={() => updateProfile({ fullname, username, website, avatar_url })}
disabled={loading}
disabled={loading || !claims?.sub}
>
{loading ? 'Loading ...' : 'Update'}
</button>
@@ -4,9 +4,7 @@ import { createClient } from '@/lib/supabase/server'
export default async function Account() {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
const { data: claimsData } = await supabase.auth.getClaims()
return <AccountForm user={user} />
return <AccountForm claims={claimsData?.claims ?? null} />
}
@@ -6,11 +6,9 @@ export async function POST(req: NextRequest) {
const supabase = await createClient()
// Check if a user's logged in
const {
data: { user },
} = await supabase.auth.getUser()
const { data: claimsData } = await supabase.auth.getClaims()
if (user) {
if (claimsData?.claims) {
await supabase.auth.signOut()
}
@@ -27,8 +27,13 @@ export async function updateSession(request: NextRequest) {
}
)
// refreshing the auth token
await supabase.auth.getUser()
// Do not run code between createServerClient and
// supabase.auth.getClaims(). A simple mistake could make it very hard to debug
// issues with users being randomly logged out.
// IMPORTANT: If you remove getClaims() and you use server-side rendering
// with the Supabase client, your users may be randomly logged out.
await supabase.auth.getClaims()
return supabaseResponse
}
File diff suppressed because it is too large. Load diff
@@ -9,20 +9,20 @@
"lint": "next lint"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",
"@supabase/supabase-js": "^2.53.0",
"next": "latest",
"react": "19.1.1",
"react-dom": "19.1.1"
"@supabase/ssr": "^0.9.0",
"@supabase/supabase-js": "^2.100.0",
"next": "^16.2.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^24.2.0",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"eslint": "^9.32.0",
"eslint-config-next": "15.4.5",
"tailwindcss": "^4.1.11",
"typescript": "^5"
"@tailwindcss/postcss": "^4.2.2",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^10.1.0",
"eslint-config-next": "16.2.1",
"tailwindcss": "^4.2.2",
"typescript": "^6.0.2"
}
}