├── .eslintrc.json ├── public ├── favicon.ico └── vercel.svg ├── pages ├── _app.js ├── api │ └── hello.js └── index.js ├── next.config.js ├── utils └── supabaseClient.js ├── package.json ├── styles ├── globals.css └── Home.module.css ├── .gitignore ├── components ├── Auth.js └── Account.js └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guillaumeduhan/supabase-nextjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css' 2 | 3 | function MyApp({ Component, pageProps }) { 4 | return 5 | } 6 | 7 | export default MyApp 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | } 6 | 7 | module.exports = nextConfig 8 | -------------------------------------------------------------------------------- /pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default function handler(req, res) { 4 | res.status(200).json({ name: 'John Doe' }) 5 | } 6 | -------------------------------------------------------------------------------- /utils/supabaseClient.js: -------------------------------------------------------------------------------- 1 | import { createClient } from '@supabase/supabase-js' 2 | 3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL 4 | const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY 5 | 6 | export const supabase = createClient(supabaseUrl, supabaseAnonKey) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@supabase/supabase-js": "^2.0.0-rc.9", 13 | "next": "12.2.5", 14 | "react": "18.2.0", 15 | "react-dom": "18.2.0" 16 | }, 17 | "devDependencies": { 18 | "eslint": "8.23.0", 19 | "eslint-config-next": "12.2.5" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | import Auth from '../components/Auth' 4 | import Account from '../components/Account' 5 | 6 | export default function Home() { 7 | const [isLoading, setIsLoading] = useState(true) 8 | const [session, setSession] = useState(null) 9 | 10 | useEffect(() => { 11 | let mounted = true 12 | 13 | async function getInitialSession() { 14 | const { 15 | data: { session }, 16 | } = await supabase.auth.getSession() 17 | 18 | // only update the react state if the component is still mounted 19 | if (mounted) { 20 | if (session) { 21 | setSession(session) 22 | } 23 | 24 | setIsLoading(false) 25 | } 26 | } 27 | 28 | getInitialSession() 29 | 30 | const { subscription } = supabase.auth.onAuthStateChange( 31 | (_event, session) => { 32 | setSession(session) 33 | } 34 | ) 35 | 36 | return () => { 37 | mounted = false 38 | 39 | subscription?.unsubscribe() 40 | } 41 | }, []) 42 | 43 | return ( 44 |
45 | {!session ? ( 46 | 47 | ) : ( 48 | 49 | )} 50 |
51 | ) 52 | } -------------------------------------------------------------------------------- /components/Auth.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | 4 | export default function Auth() { 5 | const [loading, setLoading] = useState(false) 6 | const [email, setEmail] = useState('') 7 | 8 | const handleLogin = async (email) => { 9 | try { 10 | setLoading(true) 11 | const { error } = await supabase.auth.signInWithOtp({ email }) 12 | if (error) throw error 13 | alert('Check your email for the login link!') 14 | } catch (error) { 15 | alert(error.error_description || error.message) 16 | } finally { 17 | setLoading(false) 18 | } 19 | } 20 | 21 | return ( 22 |
23 |
24 |

Supabase + Next.js

25 |

26 | Sign in via magic link with your email below 27 |

28 |
29 | setEmail(e.target.value)} 35 | /> 36 |
37 |
38 | 48 |
49 |
50 |
51 | ) 52 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /components/Account.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { supabase } from '../utils/supabaseClient' 3 | 4 | export default function Account({ session }) { 5 | const [loading, setLoading] = useState(true) 6 | const [username, setUsername] = useState(null) 7 | const [website, setWebsite] = useState(null) 8 | const [avatar_url, setAvatarUrl] = useState(null) 9 | 10 | useEffect(() => { 11 | getProfile() 12 | }, [session]) 13 | 14 | async function getCurrentUser() { 15 | const { 16 | data: { session }, 17 | error, 18 | } = await supabase.auth.getSession() 19 | 20 | if (error) { 21 | throw error 22 | } 23 | 24 | if (!session?.user) { 25 | throw new Error('User not logged in') 26 | } 27 | 28 | return session.user 29 | } 30 | 31 | async function getProfile() { 32 | try { 33 | setLoading(true) 34 | const user = await getCurrentUser() 35 | 36 | let { data, error, status } = await supabase 37 | .from('profiles') 38 | .select(`username, website, avatar_url`) 39 | .eq('id', user.id) 40 | .single() 41 | 42 | if (error && status !== 406) { 43 | throw error 44 | } 45 | 46 | if (data) { 47 | setUsername(data.username) 48 | setWebsite(data.website) 49 | setAvatarUrl(data.avatar_url) 50 | } 51 | } catch (error) { 52 | alert(error.message) 53 | } finally { 54 | setLoading(false) 55 | } 56 | } 57 | 58 | async function updateProfile({ username, website, avatar_url }) { 59 | try { 60 | setLoading(true) 61 | const user = await getCurrentUser() 62 | 63 | const updates = { 64 | id: user.id, 65 | username, 66 | website, 67 | avatar_url, 68 | updated_at: new Date(), 69 | } 70 | 71 | let { error } = await supabase.from('profiles').upsert(updates) 72 | 73 | if (error) { 74 | throw error 75 | } 76 | } catch (error) { 77 | alert(error.message) 78 | } finally { 79 | setLoading(false) 80 | } 81 | } 82 | 83 | return ( 84 |
85 |
86 | 87 | 88 |
89 |
90 | 91 | setUsername(e.target.value)} 96 | /> 97 |
98 |
99 | 100 | setWebsite(e.target.value)} 105 | /> 106 |
107 | 108 |
109 | 116 |
117 | 118 |
119 | 125 |
126 |
127 | ) 128 | } --------------------------------------------------------------------------------