mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
docs: Remove leftover auth-ui components (#45931)
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Documentation** * Updated authentication guide for Kotlin Compose Multiplatform with revised dependency configuration. * Enhanced user-management example README with improved project structure, deployment steps, and resources. * **Chores** * Refactored example applications to use custom authentication forms instead of pre-built Auth UI components. * Removed unused authentication UI dependencies from multiple example projects. <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45931) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Cemal Kılıç <cemalkilic@users.noreply.github.com>
This commit is contained in:
1 parent
5f32b3ec6b
commit
8b01d388b9
12 files changed
+476
-75
No files matched your search
@@ -723,7 +723,6 @@ curl -X PATCH "https://api.supabase.com/v1/projects/$PROJECT_REF/config/auth" \
|
||||
dependencies {
|
||||
implementation("io.github.jan-tennert.supabase:gotrue-kt:VERSION")
|
||||
implementation("io.github.jan-tennert.supabase:compose-auth:VERSION")
|
||||
implementation("io.github.jan-tennert.supabase:compose-auth-ui:VERSION") // Optional, for UI components
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@supabase/auth-ui-react": "^0.2.1",
|
||||
"@supabase/supabase-js": "^2",
|
||||
"@testing-library/jest-dom": "^5.16.3",
|
||||
"@testing-library/react": "^12.1.4",
|
||||
|
||||
@@ -1,14 +1,97 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import JSONInput from 'react-json-editor-ajrm'
|
||||
import locale from 'react-json-editor-ajrm/locale/en'
|
||||
import { supabase } from './utils/supabaseClient'
|
||||
|
||||
import { functionsList } from './functionsList'
|
||||
import { supabase } from './utils/supabaseClient'
|
||||
import { useUser } from './utils/userContext'
|
||||
|
||||
const sampleObject = { name: 'world' }
|
||||
|
||||
function AuthForm() {
|
||||
const [mode, setMode] = useState('sign-in')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [message, setMessage] = useState(null)
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
if (mode === 'sign-in') {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
if (error) setError(error.message)
|
||||
} else {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { emailRedirectTo: `${window.location.origin}/` },
|
||||
})
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
} else if (data.user && !data.session) {
|
||||
setMessage('Check your email for a confirmation link to complete sign up.')
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className="flex max-w-sm flex-col gap-3">
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
className="rounded border border-gray-300 px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete={mode === 'sign-in' ? 'current-password' : 'new-password'}
|
||||
className="rounded border border-gray-300 px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="rounded bg-green-500 py-2 px-4 font-bold text-white hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Loading...' : mode === 'sign-in' ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
}}
|
||||
className="text-sm text-blue-600 underline"
|
||||
>
|
||||
{mode === 'sign-in' ? 'Need an account? Sign up' : 'Already have an account? Sign in'}
|
||||
</button>
|
||||
{error && <p className="text-sm text-red-600">{error}</p>}
|
||||
{message && <p className="text-sm text-gray-700">{message}</p>}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { user } = Auth.useUser()
|
||||
const { user } = useUser()
|
||||
const [supaFunction, setSupaFunction] = useState(functionsList[0])
|
||||
const [requestJson, setRequestJson] = useState(sampleObject)
|
||||
const [responseJson, setResponseJson] = useState({})
|
||||
@@ -43,7 +126,7 @@ function App() {
|
||||
font-normal
|
||||
text-gray-700
|
||||
transition
|
||||
ease-in-out
|
||||
ease-in-out
|
||||
focus:bg-white focus:text-gray-700 focus:outline-none"
|
||||
onChange={(e) => setSupaFunction(e.target.value)}
|
||||
>
|
||||
@@ -89,7 +172,7 @@ function App() {
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} />
|
||||
<AuthForm />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import './index.css'
|
||||
|
||||
import App from './App'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { supabase } from './utils/supabaseClient'
|
||||
import { UserProvider } from './utils/userContext'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<Auth.UserContextProvider supabaseClient={supabase}>
|
||||
<UserProvider>
|
||||
<App />
|
||||
</Auth.UserContextProvider>
|
||||
</UserProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
import { supabase } from './supabaseClient'
|
||||
|
||||
const UserContext = createContext({ user: null, session: null })
|
||||
|
||||
export function UserProvider({ children }) {
|
||||
const [session, setSession] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => setSession(session))
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => setSession(session))
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<UserContext.Provider value={{ user: session?.user ?? null, session }}>
|
||||
{children}
|
||||
</UserContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useUser = () => useContext(UserContext)
|
||||
@@ -10,7 +10,6 @@
|
||||
"format": "prettier --cache --write \"pages/**/*.{js,jsx,ts,tsx,css,md,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/ssr": "latest",
|
||||
"@supabase/supabase-js": "^2",
|
||||
"next": "^13.1.6",
|
||||
|
||||
@@ -1,20 +1,89 @@
|
||||
import { useSupabaseClient, useUser } from '@/lib/supabase-context'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import type { NextPage } from 'next'
|
||||
import { FormEvent, useState } from 'react'
|
||||
|
||||
import styles from '../styles/Home.module.css'
|
||||
import { useSupabaseClient, useUser } from '@/lib/supabase-context'
|
||||
|
||||
type Mode = 'sign-in' | 'sign-up'
|
||||
|
||||
const LoginPage: NextPage = () => {
|
||||
const supabaseClient = useSupabaseClient()
|
||||
const user = useUser()
|
||||
|
||||
const [mode, setMode] = useState<Mode>('sign-in')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setMessage(null)
|
||||
setError(null)
|
||||
|
||||
if (mode === 'sign-in') {
|
||||
const { error } = await supabaseClient.auth.signInWithPassword({ email, password })
|
||||
if (error) setError(error.message)
|
||||
} else {
|
||||
const { data, error } = await supabaseClient.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { emailRedirectTo: `${window.location.origin}/` },
|
||||
})
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
} else if (data.user && !data.session) {
|
||||
setMessage('Check your email for a confirmation link to complete sign up.')
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<Auth
|
||||
redirectTo="http://localhost:3000/"
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
supabaseClient={supabaseClient}
|
||||
/>
|
||||
<form onSubmit={handleSubmit} style={{ display: 'grid', gap: 12, minWidth: 280 }}>
|
||||
<h1>{mode === 'sign-in' ? 'Sign in' : 'Sign up'}</h1>
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</label>
|
||||
<label style={{ display: 'grid', gap: 4 }}>
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete={mode === 'sign-in' ? 'current-password' : 'new-password'}
|
||||
/>
|
||||
</label>
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Loading...' : mode === 'sign-in' ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
}}
|
||||
>
|
||||
{mode === 'sign-in' ? 'Need an account? Sign up' : 'Already have an account? Sign in'}
|
||||
</button>
|
||||
{error && <p style={{ color: 'crimson' }}>{error}</p>}
|
||||
{message && <p>{message}</p>}
|
||||
</form>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@
|
||||
"start": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/auth-ui-shared": "^0.1.8",
|
||||
"@supabase/supabase-js": "^2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "latest",
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { supabase } from 'lib/Store'
|
||||
import { Auth } from '@supabase/auth-ui-react'
|
||||
import { ThemeSupa } from '@supabase/auth-ui-shared'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const Home = () => {
|
||||
const [session, setSession] = useState(null)
|
||||
const [mode, setMode] = useState('sign-in')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
const [message, setMessage] = useState(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
||||
@@ -20,16 +24,113 @@ const Home = () => {
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault()
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
if (mode === 'sign-in') {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
if (error) setError(error.message)
|
||||
} else {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { emailRedirectTo: `${window.location.origin}/` },
|
||||
})
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
} else if (data.user && !data.session) {
|
||||
setMessage('Check your email for a confirmation link to complete sign up.')
|
||||
}
|
||||
}
|
||||
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
const handleGitHubSignIn = async () => {
|
||||
setIsLoading(true)
|
||||
setError(null)
|
||||
const { error } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: { redirectTo: `${window.location.origin}/` },
|
||||
})
|
||||
if (error) setError(error.message)
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center p-4 bg-gray-300">
|
||||
<div className="w-full sm:w-1/2 xl:w-1/3">
|
||||
<div className="border-teal p-8 border-t-12 bg-white mb-6 rounded-lg shadow-lg bg-white">
|
||||
<Auth
|
||||
supabaseClient={supabase}
|
||||
appearance={{ theme: ThemeSupa }}
|
||||
providers={['github']}
|
||||
/>
|
||||
<div className="border-teal p-8 border-t-12 bg-white mb-6 rounded-lg shadow-lg">
|
||||
<h1 className="text-2xl font-bold mb-4">
|
||||
{mode === 'sign-in' ? 'Sign in' : 'Sign up'}
|
||||
</h1>
|
||||
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3">
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
className="border rounded px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete={mode === 'sign-in' ? 'current-password' : 'new-password'}
|
||||
className="border rounded px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
className="bg-blue-600 text-white rounded px-3 py-2 disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Loading...' : mode === 'sign-in' ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="flex items-center gap-2 my-4">
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
<span className="text-xs text-gray-500">or</span>
|
||||
<div className="flex-1 h-px bg-gray-200" />
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGitHubSignIn}
|
||||
disabled={isLoading}
|
||||
className="w-full bg-gray-900 text-white rounded px-3 py-2"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Continue with GitHub'}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
}}
|
||||
className="mt-4 text-sm text-blue-600 underline"
|
||||
>
|
||||
{mode === 'sign-in' ? 'Need an account? Sign up' : 'Already have an account? Sign in'}
|
||||
</button>
|
||||
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
{message && <p className="mt-3 text-sm text-gray-700">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/font": "13.1.6",
|
||||
"@supabase/auth-ui-react": "^0.4.7",
|
||||
"@supabase/ssr": "latest",
|
||||
"@supabase/supabase-js": "^2",
|
||||
"@types/node": "18.14.0",
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import Head from 'next/head'
|
||||
import { supabase } from '@/lib/initSupabase'
|
||||
import { Auth, ThemeSupa } from '@supabase/auth-ui-react'
|
||||
import TodoList from '@/components/TodoList'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
|
||||
import TodoList from '@/components/TodoList'
|
||||
import { supabase } from '@/lib/initSupabase'
|
||||
|
||||
type Mode = 'sign-in' | 'sign-up'
|
||||
|
||||
export default function Home() {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [mode, setMode] = useState<Mode>('sign-in')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data: { session } }) => setSession(session))
|
||||
@@ -18,6 +26,31 @@ export default function Home() {
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
if (mode === 'sign-in') {
|
||||
const { error } = await supabase.auth.signInWithPassword({ email, password })
|
||||
if (error) setError(error.message)
|
||||
} else {
|
||||
const { data, error } = await supabase.auth.signUp({
|
||||
email,
|
||||
password,
|
||||
options: { emailRedirectTo: `${window.location.origin}/` },
|
||||
})
|
||||
if (error) {
|
||||
setError(error.message)
|
||||
} else if (data.user && !data.session) {
|
||||
setMessage('Check your email for a confirmation link to complete sign up.')
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
@@ -32,9 +65,55 @@ export default function Home() {
|
||||
<div className="w-full h-full flex justify-center items-center p-4">
|
||||
<div className="w-full h-full sm:h-auto sm:w-2/5 max-w-sm p-5 bg-white shadow flex flex-col text-base">
|
||||
<span className="font-sans text-4xl text-center pb-2 mb-1 border-b mx-4 align-center">
|
||||
Login
|
||||
{mode === 'sign-in' ? 'Sign In' : 'Sign up'}
|
||||
</span>
|
||||
<Auth supabaseClient={supabase} appearance={{ theme: ThemeSupa }} theme="dark" />
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-3 mt-4">
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
autoComplete="email"
|
||||
className="border rounded px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm">
|
||||
Password
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
minLength={6}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
autoComplete={mode === 'sign-in' ? 'current-password' : 'new-password'}
|
||||
className="border rounded px-2 py-1"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="btn-black w-full disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Loading...' : mode === 'sign-in' ? 'Sign in' : 'Sign up'}
|
||||
</button>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMode(mode === 'sign-in' ? 'sign-up' : 'sign-in')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
}}
|
||||
className="mt-3 text-sm text-blue-600 underline"
|
||||
>
|
||||
{mode === 'sign-in'
|
||||
? 'Need an account? Sign up'
|
||||
: 'Already have an account? Sign in'}
|
||||
</button>
|
||||
{error && <p className="mt-3 text-sm text-red-600">{error}</p>}
|
||||
{message && <p className="mt-3 text-sm text-gray-700">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,36 @@
|
||||
# Supabase Next.js Auth & User Management Starter
|
||||
|
||||
This example will set you up for a very common situation: users can sign up or sign in and then update their account with public profile information, including a profile image.
|
||||
This example sets you up for a very common situation: users can sign up or sign in and then update their account with public profile information, including a profile image.
|
||||
|
||||
This demonstrates how to use:
|
||||
|
||||
- User signups using Supabase [Auth](https://supabase.com/auth).
|
||||
- Supabase [SSR Auth for Next.js](https://supabase.com/docs/guides/auth/server-side/nextjs).
|
||||
- Supabase [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-ui).
|
||||
- User avatar images using Supabase [Storage](https://supabase.com/storage)
|
||||
- Public profiles restricted with [Policies](https://supabase.com/docs/guides/auth#policies).
|
||||
- Frontend using [Next.js](<[nextjs.org/](https://nextjs.org/)>).
|
||||
- Supabase [SSR Auth for Next.js](https://supabase.com/docs/guides/auth/server-side/nextjs) with the Next.js App Router and Server Actions.
|
||||
- User avatar images using Supabase [Storage](https://supabase.com/storage).
|
||||
- Public profiles restricted with [Row Level Security policies](https://supabase.com/docs/guides/auth/row-level-security).
|
||||
- Frontend using [Next.js](https://nextjs.org/) (App Router) with React 19 and Tailwind CSS v4.
|
||||
|
||||
## Technologies used
|
||||
|
||||
- Frontend:
|
||||
- [Next.js](https://github.com/vercel/next.js) - a React framework for production.
|
||||
- [Supabase.js](https://supabase.com/docs/library/getting-started) for user management and realtime data syncing.
|
||||
- Supabase [`@supabase/ssr`](https://supabase.com/docs/guides/auth/server-side/nextjs) for SSR auth.
|
||||
- Supabase [pre-built Auth UI for React](https://supabase.com/docs/guides/auth/auth-ui).
|
||||
- [Next.js](https://github.com/vercel/next.js) (App Router) — a React framework for production.
|
||||
- [`@supabase/ssr`](https://supabase.com/docs/guides/auth/server-side/nextjs) for cookie-based SSR auth, used from both Server Components and Server Actions.
|
||||
- [`@supabase/supabase-js`](https://supabase.com/docs/library/getting-started) for the browser client and realtime data.
|
||||
- [Tailwind CSS v4](https://tailwindcss.com/) for styling.
|
||||
- Backend:
|
||||
- [supabase.com/dashboard](https://supabase.com/dashboard/): hosted Postgres database with restful API for usage with Supabase.js.
|
||||
- [supabase.com/dashboard](https://supabase.com/dashboard/) — hosted Postgres database with a REST API, Auth, and Storage.
|
||||
- Local development via the [Supabase CLI](https://supabase.com/docs/guides/cli).
|
||||
|
||||
## Project structure
|
||||
|
||||
- `app/login/` — login and signup form. The form posts to Server Actions in `app/login/actions.ts` that call `supabase.auth.signInWithPassword()` and `supabase.auth.signUp()`.
|
||||
- `app/account/` — protected profile page. Uses a Supabase server client to check the session and renders an account form with avatar upload.
|
||||
- `app/auth/confirm/route.ts` — handles the email confirmation callback by verifying the OTP token and redirecting.
|
||||
- `app/auth/signout/route.ts` — server route that signs the user out.
|
||||
- `lib/supabase/client.ts` — browser client (`createBrowserClient`).
|
||||
- `lib/supabase/server.ts` — server client (`createServerClient`) wired up to Next.js cookies.
|
||||
- `supabase/migrations/` — database schema for the `profiles` table, RLS policies, the `handle_new_user` trigger, and the `avatars` storage bucket.
|
||||
- `supabase/config.toml` — local Supabase configuration used by `npx supabase start`.
|
||||
|
||||
## Instant deploy
|
||||
|
||||
@@ -27,43 +38,79 @@ The Vercel deployment will guide you through creating a Supabase account and pro
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-user-management&project-name=supabase-nextjs-user-management&repository-name=supabase-nextjs-user-management&integration-ids=oac_VqOgBHqhEoFTPzGkPd7L0iH6&external-id=https%3A%2F%2Fgithub.com%2Fsupabase%2Fsupabase%2Ftree%2Fmaster%2Fexamples%2Fuser-management%2Fnextjs-user-management)
|
||||
|
||||
### 1. Create new project
|
||||
## Run locally
|
||||
|
||||
Sign up to Supabase - [https://supabase.com/dashboard](https://supabase.com/dashboard) and create a new project. Wait for your database to start.
|
||||
### 1. Install dependencies
|
||||
|
||||
### 2. Run "User Management" Quickstart
|
||||
Requires Node.js 20+ and `npx` (bundled with npm).
|
||||
|
||||
Once your database has started, head over to your project's `SQL Editor` and run the "User Management Starter" quickstart. On the `SQL editor` page, scroll down until you see `User Management Starter: Sets up a public Profiles table which you can access with your API`. Click that, then click `RUN` to execute that query and create a new `profiles` table. When that's finished, head over to the `Table Editor` and see your new `profiles` table.
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### 3. Get the URL and Key
|
||||
### 2. Start a local Supabase stack
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your API URL and `anon` key, you'll need these in the next step.
|
||||
The example includes a `supabase/` directory with the schema and config needed to run a local stack via the Supabase CLI.
|
||||
|
||||
The `anon` key is your client-side API key. It allows "anonymous access" to your database, until the user has logged in. Once they have logged in, the keys will switch to the user's own login token. This enables row level security for your data. Read more about this [below](#postgres-row-level-security).
|
||||
```bash
|
||||
npx supabase start
|
||||
```
|
||||
|
||||

|
||||
This boots Postgres, Auth, Storage, and Supabase Studio locally and runs the migrations in `supabase/migrations/`. When it finishes, it prints your local API URL and keys.
|
||||
|
||||
**_NOTE_**: The `secret` key has full access to your data, bypassing any security policies. These keys have to be kept secret and are meant to be used in server environments and never on a client or browser.
|
||||
### 3. Configure environment variables
|
||||
|
||||
## Supabase details
|
||||
Copy the development env template:
|
||||
|
||||
### Using a Remote Supabase Project
|
||||
```bash
|
||||
cp .env.example .env.local
|
||||
```
|
||||
|
||||
1. Create or select a project on [Supabase Dashboard](https://supabase.com/dashboard).
|
||||
2. Copy and fill the dotenv template `cp .env.production.example .env.production`
|
||||
3. Link the remote project to your local environment:
|
||||
The defaults in `.env.example` already match the local stack (API URL `http://127.0.0.1:54321` and the demo publishable key). Update them if your local ports differ.
|
||||
|
||||
### 4. Start the dev server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) to use the app.
|
||||
|
||||
## Using a remote Supabase project
|
||||
|
||||
### 1. Create a project
|
||||
|
||||
Sign up at [https://supabase.com/dashboard](https://supabase.com/dashboard) and create a new project. Wait for your database to start.
|
||||
|
||||
### 2. Get the URL and publishable key
|
||||
|
||||
Go to the Project Settings (the cog icon), open the API tab, and find your **Project URL** and **publishable key**.
|
||||
|
||||
The `publishable` key is your client-side API key. It allows "anonymous access" to your database until the user logs in. Once they log in, the user's own JWT is used, which enables Row Level Security to scope data per user. Read more [below](#postgres-row-level-security).
|
||||
|
||||
> **Note:** The `secret` (service role) key has full access to your data and bypasses all security policies. Keep it in server environments only — never expose it to the client or browser.
|
||||
|
||||
### 3. Link and push the schema
|
||||
|
||||
Copy the production env template and fill it in with your project URL, publishable key, and the URL(s) you want to allow as redirect targets:
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env.production
|
||||
```
|
||||
|
||||
Link your local checkout to the remote project:
|
||||
|
||||
```bash
|
||||
SUPABASE_ENV=production npx supabase@latest link --project-ref <your-project-ref>
|
||||
```
|
||||
|
||||
3. Sync the configuration:
|
||||
Push the `supabase/config.toml` settings (Auth site URL, redirect URLs, etc.):
|
||||
|
||||
```bash
|
||||
SUPABASE_ENV=production npx supabase@latest config push
|
||||
```
|
||||
|
||||
4. Sync the database schema:
|
||||
Push the database schema in `supabase/migrations/`:
|
||||
|
||||
```bash
|
||||
SUPABASE_ENV=production npx supabase@latest db push
|
||||
@@ -81,19 +128,19 @@ Supabase integrates seamlessly with Vercel's preview branches, giving each branc
|
||||
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`
|
||||
|
||||
3. Create a new branch, make changes (e.g., update `max_frequency`), and push the branch to Git.
|
||||
- Open a pull request to trigger Vercel + Supabase integration.
|
||||
- Open a pull request to trigger the Vercel + Supabase integration.
|
||||
- Upon successful deployment, the preview environment reflects the changes.
|
||||
|
||||

|
||||
|
||||
### Postgres Row level security
|
||||
## Postgres Row Level Security
|
||||
|
||||
This project uses very high-level Authorization using Postgres' Row Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema, and some helper functions.
|
||||
This project uses high-level authorization via Postgres' Row Level Security.
|
||||
When you start a Postgres database on Supabase, we populate it with an `auth` schema and some helper functions.
|
||||
When a user logs in, they are issued a JWT with the role `authenticated` and their UUID.
|
||||
We can use these details to provide fine-grained control over what each user can and cannot do.
|
||||
|
||||
This is a trimmed-down schema, with the policies:
|
||||
The schema and policies that this example uses (see `supabase/migrations/20221017024722_init.sql`):
|
||||
|
||||
```sql
|
||||
-- Create a table for public profiles
|
||||
@@ -151,21 +198,21 @@ create policy "Anyone can update their own avatar." on storage.objects
|
||||
for update using ( auth.uid() = owner ) with check (bucket_id = 'avatars');
|
||||
```
|
||||
|
||||
## More Supabase Examples & Resources
|
||||
## More Supabase examples & resources
|
||||
|
||||
## Examples
|
||||
### Examples
|
||||
|
||||
These official examples are maintained by the Supabase team:
|
||||
|
||||
- [Next.js Subscription Payments Starter](https://github.com/vercel/nextjs-subscription-payments)
|
||||
- [Next.js Slack Clone](https://github.com/supabase/supabase/tree/master/examples/slack-clone/nextjs-slack-clone)
|
||||
- [Next.js 13 Data Fetching](https://github.com/supabase/supabase/tree/master/examples/caching/with-nextjs-13)
|
||||
- [Next.js Data Fetching](https://github.com/supabase/supabase/tree/master/examples/caching/with-nextjs-13)
|
||||
- [And more...](https://github.com/supabase/supabase/tree/master/examples)
|
||||
|
||||
## Other resources
|
||||
### Other resources
|
||||
|
||||
- [[Docs] Next.js User Management Quickstart](https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs)
|
||||
- [[Egghead.io] Build a SaaS product with Next.js, Supabase and Stripe](https://egghead.io/courses/build-a-saas-product-with-next-js-supabase-and-stripe-61f2bc20)
|
||||
- [[Docs] Server-Side Auth for Next.js](https://supabase.com/docs/guides/auth/server-side/nextjs)
|
||||
- [[Blog] Fetching and caching Supabase data in Next.js 13 Server Components](https://supabase.com/blog/fetching-and-caching-supabase-data-in-next-js-server-components)
|
||||
|
||||
## Authors
|
||||
|
||||
Reference in new issue
Block a user