├── .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 |
--------------------------------------------------------------------------------
/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 | }
--------------------------------------------------------------------------------